// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. #include "BlueprintNativeCodeGenPCH.h" #include "NativeCodeGenerationTool.h" #include "Dialogs.h" #include "Engine/BlueprintGeneratedClass.h" #include "Engine/UserDefinedEnum.h" #include "Engine/UserDefinedStruct.h" #include "SourceCodeNavigation.h" #include "DesktopPlatformModule.h" // for InvalidateMakefiles() //#include "Editor/KismetCompiler/Public/BlueprintCompilerCppBackendInterface.h" #define LOCTEXT_NAMESPACE "NativeCodeGenerationTool" // // THE CODE SHOULD BE MOVED TO GAMEPROJECTGENERATION // struct FGeneratedCodeData { FGeneratedCodeData(UBlueprint& InBlueprint) : Blueprint(&InBlueprint) { FName GeneratedClassName, SkeletonClassName; InBlueprint.GetBlueprintClassNames(GeneratedClassName, SkeletonClassName); ClassName = GeneratedClassName.ToString(); GatherUserDefinedDependencies(InBlueprint); } FString TypeDependencies; FString ErrorString; FString ClassName; TWeakObjectPtr Blueprint; TSet DependentObjects; void GatherUserDefinedDependencies(UBlueprint& InBlueprint) { TArray ReferencedObjects; { FReferenceFinder ReferenceFinder(ReferencedObjects, NULL, false, false, false, false); { TArray ObjectsToCheck; GetObjectsWithOuter(InBlueprint.GeneratedClass, ObjectsToCheck, true); for (auto Obj : ObjectsToCheck) { if (IsValid(Obj)) { ReferenceFinder.FindReferences(Obj); } } } if (InBlueprint.GeneratedClass) { ReferenceFinder.FindReferences(InBlueprint.GeneratedClass->GetDefaultObject(false)); } for (UClass* Class = InBlueprint.GeneratedClass->GetSuperClass(); Class && !Class->HasAnyClassFlags(CLASS_Native); Class = Class->GetSuperClass()) { ReferencedObjects.Add(Class); } for (auto& ImplementedInterface : InBlueprint.GeneratedClass->Interfaces) { if (ImplementedInterface.Class && !ImplementedInterface.Class->HasAnyClassFlags(CLASS_Native)) { ReferencedObjects.Add(ImplementedInterface.Class); } } } TypeDependencies.Empty(); for (auto Obj : ReferencedObjects) { if (IsValid(Obj) && !Obj->IsIn(InBlueprint.GeneratedClass) && (Obj != InBlueprint.GeneratedClass)) { if (Obj->IsA() || Obj->IsA() || Obj->IsA()) { TypeDependencies += Obj->GetPathName(); TypeDependencies += TEXT("\n"); DependentObjects.Add(Obj); } } } DependentObjects.Add(InBlueprint.GeneratedClass); if (TypeDependencies.IsEmpty()) { TypeDependencies += LOCTEXT("NoNonNativeDependencies", "No non-native dependencies.").ToString(); } } static FString DefaultHeaderDir() { auto DefaultSourceDir = FPaths::ConvertRelativePathToFull(FPaths::GameSourceDir()); return FPaths::Combine(*DefaultSourceDir, FApp::GetGameName(), TEXT("Public")); } static FString DefaultSourceDir() { auto DefaultSourceDir = FPaths::ConvertRelativePathToFull(FPaths::GameSourceDir()); return FPaths::Combine(*DefaultSourceDir, FApp::GetGameName(), TEXT("Private")); } FString HeaderFileName() const { return ClassName + TEXT(".h"); } FString SourceFileName() const { return ClassName + TEXT(".cpp"); } bool Save(const FString& HeaderDirPath, const FString& CppDirPath) { if (!Blueprint.IsValid()) { ErrorString += LOCTEXT("InvalidBlueprint", "Invalid Blueprint\n").ToString(); return false; } const int WorkParts = 4 + 2 * DependentObjects.Num(); FScopedSlowTask SlowTask(WorkParts, LOCTEXT("GeneratingCppFiles", "Generating C++ files..")); SlowTask.MakeDialog(); SlowTask.EnterProgressFrame(); TArray CreatedFiles; for(auto Obj : DependentObjects) { TSharedPtr HeaderSource(new FString()); TSharedPtr CppSource(new FString()); //BFBlueprintNativeCodeGenUtils::GenerateCppCode(Obj, HeaderSource, CppSource); SlowTask.EnterProgressFrame(); const FString FullHeaderFilename = FPaths::Combine(*HeaderDirPath, *(Obj->GetName() + TEXT(".h"))); const bool bHeaderSaved = FFileHelper::SaveStringToFile(*HeaderSource, *FullHeaderFilename); if (!bHeaderSaved) { ErrorString += FString::Printf(*LOCTEXT("HeaderNotSaved", "Header file wasn't saved. Check log for details. %s\n").ToString(), *Obj->GetPathName()); } else { CreatedFiles.Add(FullHeaderFilename); } SlowTask.EnterProgressFrame(); if (!CppSource->IsEmpty()) { const FString NewCppFilename = FPaths::Combine(*CppDirPath, *(Obj->GetName() + TEXT(".cpp"))); const bool bCppSaved = FFileHelper::SaveStringToFile(*CppSource, *NewCppFilename); if (!bCppSaved) { ErrorString += FString::Printf(*LOCTEXT("CppNotSaved", "Cpp file wasn't saved. Check log for details. %s\n").ToString(), *Obj->GetPathName()); } else { CreatedFiles.Add(NewCppFilename); } } } SlowTask.EnterProgressFrame(); bool bGenerateProjectFiles = true; { bool bProjectHadCodeFiles = false; { TArray OutProjectCodeFilenames; IFileManager::Get().FindFilesRecursive(OutProjectCodeFilenames, *FPaths::GameSourceDir(), TEXT("*.h"), true, false, false); IFileManager::Get().FindFilesRecursive(OutProjectCodeFilenames, *FPaths::GameSourceDir(), TEXT("*.cpp"), true, false, false); bProjectHadCodeFiles = OutProjectCodeFilenames.Num() > 0; } TArray CreatedFilesForExternalAppRead; CreatedFilesForExternalAppRead.Reserve(CreatedFiles.Num()); for (const FString& CreatedFile : CreatedFiles) { CreatedFilesForExternalAppRead.Add(IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*CreatedFile)); } // First see if we can avoid a full generation by adding the new files to an already open project if (bProjectHadCodeFiles && FSourceCodeNavigation::AddSourceFiles(CreatedFilesForExternalAppRead)) { // We successfully added the new files to the solution, but we still need to run UBT with -gather to update any UBT makefiles if (FDesktopPlatformModule::Get()->InvalidateMakefiles(FPaths::RootDir(), FPaths::GetProjectFilePath(), GWarn)) { // We managed the gather, so we can skip running the full generate bGenerateProjectFiles = false; } } } SlowTask.EnterProgressFrame(); bool bProjectFileUpdated = true; if (bGenerateProjectFiles) { // Generate project files if we happen to be using a project file. if (!FDesktopPlatformModule::Get()->GenerateProjectFiles(FPaths::RootDir(), FPaths::GetProjectFilePath(), GWarn)) { ErrorString += LOCTEXT("FailedToGenerateProjectFiles", "Failed to generate project files.").ToString(); bProjectFileUpdated = false; } } return ErrorString.IsEmpty(); } }; class SSimpleDirectoryPicker : public SCompoundWidget { public: SLATE_BEGIN_ARGS(SSimpleDirectoryPicker){} SLATE_ARGUMENT(FString, Directory) SLATE_ARGUMENT(FString, File) SLATE_ARGUMENT(FText, Message) SLATE_ATTRIBUTE(bool, IsEnabled) SLATE_END_ARGS() FString File; FString Directory; FText Message; TSharedPtr EditableTextBox; FString GetFilePath() const { return FPaths::Combine(*Directory, *File); } FText GetFilePathText() const { return FText::FromString(GetFilePath()); } FReply BrowseHeaderDirectory() { PromptUserForDirectory(Directory, Message.ToString(), Directory); //workaround for UI problem EditableTextBox->SetText(TAttribute::Create(TAttribute::FGetter::CreateSP(this, &SSimpleDirectoryPicker::GetFilePathText))); return FReply::Handled(); } void Construct(const FArguments& InArgs) { Directory = InArgs._Directory; File = InArgs._File; Message = InArgs._Message; TSharedPtr OpenButton; ChildSlot [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0f) .Padding(4.0f, 0.0f, 0.0f, 0.0f) [ SAssignNew(EditableTextBox, SEditableTextBox) .Text(this, &SSimpleDirectoryPicker::GetFilePathText) .IsReadOnly(true) ] + SHorizontalBox::Slot() .AutoWidth() .Padding(4.0f, 0.0f, 0.0f, 0.0f) [ SAssignNew(OpenButton, SButton) .ToolTipText(Message) .OnClicked(this, &SSimpleDirectoryPicker::BrowseHeaderDirectory) [ SNew(SImage) .Image(FEditorStyle::GetBrush("PropertyWindow.Button_Ellipsis")) ] ] ]; OpenButton->SetEnabled(InArgs._IsEnabled); } }; class SNativeCodeGenerationDialog : public SCompoundWidget { public: SLATE_BEGIN_ARGS(SNativeCodeGenerationDialog){} SLATE_ARGUMENT(TSharedPtr, ParentWindow) SLATE_ARGUMENT(TSharedPtr, GeneratedCodeData) SLATE_END_ARGS() private: // Child widgets TSharedPtr HeaderDirectoryBrowser; TSharedPtr SourceDirectoryBrowser; TSharedPtr ErrorWidget; // TWeakPtr WeakParentWindow; TSharedPtr GeneratedCodeData; bool bSaved; void CloseParentWindow() { auto ParentWindow = WeakParentWindow.Pin(); if (ParentWindow.IsValid()) { ParentWindow->RequestDestroyWindow(); } } bool IsEditable() const { return !bSaved && GeneratedCodeData->ErrorString.IsEmpty(); } FReply OnButtonClicked() { if (IsEditable()) { bSaved = GeneratedCodeData->Save(HeaderDirectoryBrowser->Directory, SourceDirectoryBrowser->Directory); ErrorWidget->SetError(GeneratedCodeData->ErrorString); } else { CloseParentWindow(); } return FReply::Handled(); } FText ButtonText() const { return IsEditable() ? LOCTEXT("Generate", "Generate") : LOCTEXT("Close", "Close"); } FText GetClassName() const { return GeneratedCodeData.IsValid() ? FText::FromString(GeneratedCodeData->ClassName) : FText::GetEmpty(); } public: void Construct(const FArguments& InArgs) { GeneratedCodeData = InArgs._GeneratedCodeData; bSaved = false; WeakParentWindow = InArgs._ParentWindow; ChildSlot [ SNew(SBorder) .Padding(4.f) .BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder")) [ SNew(SVerticalBox) + SVerticalBox::Slot() .Padding(4.0f) .AutoHeight() [ SNew(STextBlock) .Text(LOCTEXT("ClassName", "Class Name")) ] + SVerticalBox::Slot() .Padding(4.0f) .AutoHeight() [ SNew(STextBlock) .Text(this, &SNativeCodeGenerationDialog::GetClassName) ] + SVerticalBox::Slot() .Padding(4.0f) .AutoHeight() [ SNew(STextBlock) .Text(LOCTEXT("HeaderPath", "Header Path")) ] + SVerticalBox::Slot() .Padding(4.0f) .AutoHeight() [ SAssignNew(HeaderDirectoryBrowser, SSimpleDirectoryPicker) .Directory(FGeneratedCodeData::DefaultHeaderDir()) .File(GeneratedCodeData->HeaderFileName()) .Message(LOCTEXT("HeaderDirectory", "Header Directory")) .IsEnabled(this, &SNativeCodeGenerationDialog::IsEditable) ] + SVerticalBox::Slot() .Padding(4.0f) .AutoHeight() [ SNew(STextBlock) .Text(LOCTEXT("SourcePath", "Source Path")) ] + SVerticalBox::Slot() .Padding(4.0f) .AutoHeight() [ SAssignNew(SourceDirectoryBrowser, SSimpleDirectoryPicker) .Directory(FGeneratedCodeData::DefaultSourceDir()) .File(GeneratedCodeData->SourceFileName()) .Message(LOCTEXT("SourceDirectory", "Source Directory")) .IsEnabled(this, &SNativeCodeGenerationDialog::IsEditable) ] + SVerticalBox::Slot() .Padding(4.0f) .AutoHeight() [ SNew(STextBlock) .Text(LOCTEXT("Dependencies", "Dependencies")) ] + SVerticalBox::Slot() .Padding(4.0f) .AutoHeight() [ SNew(SBox) .WidthOverride(360.0f) .HeightOverride(200.0f) [ SNew(SMultiLineEditableTextBox) .IsReadOnly(true) .Text(FText::FromString(GeneratedCodeData->TypeDependencies)) ] ] + SVerticalBox::Slot() .Padding(4.0f) .AutoHeight() [ SAssignNew(ErrorWidget, SErrorText) ] + SVerticalBox::Slot() .Padding(4.0f) .AutoHeight() .HAlign(HAlign_Right) [ SNew(SButton) .Text(this, &SNativeCodeGenerationDialog::ButtonText) .OnClicked(this, &SNativeCodeGenerationDialog::OnButtonClicked) ] ] ]; ErrorWidget->SetError(GeneratedCodeData->ErrorString); } }; void FNativeCodeGenerationTool::Open(UBlueprint& Blueprint, TSharedRef< class FBlueprintEditor> Editor) { TSharedRef GeneratedCodeData(new FGeneratedCodeData(Blueprint)); TSharedRef PickerWindow = SNew(SWindow) .Title(LOCTEXT("GenerateNativeCode", "Generate Native Code")) .SizingRule(ESizingRule::Autosized) .ClientSize(FVector2D(0.f, 300.f)) .SupportsMaximize(false) .SupportsMinimize(false); TSharedRef CodeGenerationDialog = SNew(SNativeCodeGenerationDialog) .ParentWindow(PickerWindow) .GeneratedCodeData(GeneratedCodeData); PickerWindow->SetContent(CodeGenerationDialog); GEditor->EditorAddModalWindow(PickerWindow); } bool FNativeCodeGenerationTool::CanGenerate(const UBlueprint& Blueprint) { return (Blueprint.Status == EBlueprintStatus::BS_UpToDate) && (Blueprint.BlueprintType == EBlueprintType::BPTYPE_Normal || Blueprint.BlueprintType == EBlueprintType::BPTYPE_FunctionLibrary) && Cast(Blueprint.GeneratedClass); } #undef LOCTEXT_NAMESPACE