// Copyright Epic Games, Inc. All Rights Reserved. #include "SProjectDialog.h" #include "Algo/Transform.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/SBoxPanel.h" #include "GameProjectGenerationModule.h" #include "TemplateCategory.h" #include "SProjectBrowser.h" #include "Widgets/Layout/SSeparator.h" #include "IDocumentation.h" #include "IDesktopPlatform.h" #include "DesktopPlatformModule.h" #include "TemplateItem.h" #include "SourceCodeNavigation.h" #include "GameProjectUtils.h" #include "Framework/Application/SlateApplication.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Framework/Commands/UIAction.h" #include "Editor.h" #include "Internationalization/BreakIterator.h" #include "Settings/EditorSettings.h" #include "Widgets/Layout/SScrollBorder.h" #include "Widgets/Layout/SScrollBox.h" #include "HardwareTargetingModule.h" #include "Widgets/Input/SSegmentedControl.h" #include "GameProjectGenerationLog.h" #include "Interfaces/IPluginManager.h" #include "HAL/PlatformFileManager.h" #include "Dialogs/SOutputLogDialog.h" #include "Misc/MessageDialog.h" #include "HAL/FileManager.h" #include "ProjectDescriptor.h" #include "Widgets/Images/SImage.h" #include "Widgets/SToolTip.h" #include "Widgets/Layout/SGridPanel.h" #include "Widgets/SOverlay.h" #include "Widgets/Input/SComboBox.h" #include "Widgets/Input/SEditableTextBox.h" #include "Widgets/Input/SButton.h" #include "SWarningOrErrorBox.h" #include "SGetSuggestedIDEWidget.h" #include "Brushes/SlateDynamicImageBrush.h" #include "Widgets/Layout/SWidgetSwitcher.h" #include "SProjectDialog.h" #include "LauncherPlatformModule.h" #include "SPrimaryButton.h" #include "Styling/StyleColors.h" #include "Styling/AppStyle.h" #define LOCTEXT_NAMESPACE "GameProjectGeneration" namespace NewProjectDialogDefs { constexpr float MajorItemWidth = 304; constexpr float MajorItemHeight = 104; constexpr float TemplateTileHeight = 153; constexpr float TemplateTileWidth = 102; constexpr float ThumbnailSize = 64.0f, ThumbnailPadding = 5.f; const FName DefaultCategoryName = "Games"; const FName BlankCategoryKey = "Default"; } TUniquePtr SProjectDialog::CustomTemplateBrush; class SMajorCategoryTile : public SCompoundWidget { SLATE_BEGIN_ARGS(SMajorCategoryTile) {} SLATE_ATTRIBUTE(bool, IsSelected) SLATE_END_ARGS() public: void Construct(const FArguments& InArgs, TSharedPtr Item) { IsSelected = InArgs._IsSelected; ChildSlot [ SNew(SOverlay) + SOverlay::Slot() [ SNew(SImage) .Image(Item->Icon) ] + SOverlay::Slot() .HAlign(HAlign_Left) .VAlign(VAlign_Bottom) .Padding(FMargin(18.0f, 8.0f)) [ SNew(STextBlock) .Font(FAppStyle::Get().GetFontStyle("HeadingExtraSmall")) .ColorAndOpacity(FLinearColor(1, 1, 1, .9f)) .TransformPolicy(ETextTransformPolicy::ToUpper) .ShadowOffset(FVector2D(1, 1)) .ShadowColorAndOpacity(FLinearColor(0, 0, 0, .75)) .Text(Item->DisplayName) .WrapTextAt(250.0f) ] + SOverlay::Slot() [ SNew(SImage) .Visibility(EVisibility::HitTestInvisible) .Image(this, &SMajorCategoryTile::GetSelectionOutlineBrush) ] ]; } private: const FSlateBrush* GetSelectionOutlineBrush() const { const bool bIsSelected = IsSelected.Get(); const bool bIsTileHovered = IsHovered(); if (bIsSelected && bIsTileHovered) { static const FName SelectedHover("ProjectBrowser.ProjectTile.SelectedHoverBorder"); return FAppStyle::Get().GetBrush(SelectedHover); } else if (bIsSelected) { static const FName Selected("ProjectBrowser.ProjectTile.SelectedBorder"); return FAppStyle::Get().GetBrush(Selected); } else if (bIsTileHovered) { static const FName Hovered("ProjectBrowser.ProjectTile.HoverBorder"); return FAppStyle::Get().GetBrush(Hovered); } return FStyleDefaults::GetNoBrush(); } private: TAttribute IsSelected; }; /** Slate tile widget for template projects */ class STemplateTile : public STableRow> { public: SLATE_BEGIN_ARGS( STemplateTile ){} SLATE_ARGUMENT(TSharedPtr, Item) SLATE_END_ARGS() private: TWeakPtr Item; public: /** Static build function */ static TSharedRef BuildTile(TSharedPtr Item, const TSharedRef& OwnerTable) { if (!ensure(Item.IsValid())) { return SNew(STableRow>, OwnerTable); } return SNew(STemplateTile, OwnerTable).Item(Item); } /** Constructs this widget with InArgs */ void Construct( const FArguments& InArgs, const TSharedRef& OwnerTable ) { check(InArgs._Item.IsValid()) Item = InArgs._Item; STableRow::FArguments TableRowArguments; TableRowArguments._SignalSelectionMode = ETableRowSignalSelectionMode::Instantaneous; STableRow::Construct( TableRowArguments .Style(FAppStyle::Get(), "ProjectBrowser.TableRow") .Padding(2.0f) .Content() [ SNew(SBorder) .Padding(FMargin(0.0f, 0.0f, 5.0f, 5.0f)) .BorderImage(FAppStyle::Get().GetBrush("ProjectBrowser.ProjectTile.DropShadow")) [ SNew(SOverlay) + SOverlay::Slot() [ SNew(SVerticalBox) // Thumbnail + SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Center) .VAlign(VAlign_Center) [ SNew(SBox) .WidthOverride(102.0f) .HeightOverride(102.0f) [ SNew(SBorder) .Padding(0.0f) .BorderImage(FAppStyle::Get().GetBrush("ProjectBrowser.ProjectTile.ThumbnailAreaBackground")) .HAlign(HAlign_Center) .VAlign(VAlign_Center) [ SNew(SImage) .Image(this, &STemplateTile::GetThumbnail) .DesiredSizeOverride(InArgs._Item->bThumbnailAsIcon ? TOptional() : FVector2D(NewProjectDialogDefs::ThumbnailSize, NewProjectDialogDefs::ThumbnailSize)) ] ] ] // Name + SVerticalBox::Slot() [ SNew(SBorder) .Padding(FMargin(NewProjectDialogDefs::ThumbnailPadding, 0)) .VAlign(VAlign_Top) .Padding(FMargin(3.0f, 3.0f)) .BorderImage_Lambda ( [this]() { const bool bIsSelected = IsSelected(); const bool bIsRowHovered = IsHovered(); if (bIsSelected && bIsRowHovered) { static const FName SelectedHover("ProjectBrowser.ProjectTile.NameAreaSelectedHoverBackground"); return FAppStyle::Get().GetBrush(SelectedHover); } else if (bIsSelected) { static const FName Selected("ProjectBrowser.ProjectTile.NameAreaSelectedBackground"); return FAppStyle::Get().GetBrush(Selected); } else if (bIsRowHovered) { static const FName Hovered("ProjectBrowser.ProjectTile.NameAreaHoverBackground"); return FAppStyle::Get().GetBrush(Hovered); } return FAppStyle::Get().GetBrush("ProjectBrowser.ProjectTile.NameAreaBackground"); } ) [ SNew(STextBlock) .Font(FAppStyle::Get().GetFontStyle("ProjectBrowser.ProjectTile.Font")) .WrapTextAt(NewProjectDialogDefs::TemplateTileWidth-4.0f) .LineBreakPolicy(FBreakIterator::CreateCamelCaseBreakIterator()) .Text(InArgs._Item->Name) .ColorAndOpacity_Lambda ( [this]() { const bool bIsSelected = IsSelected(); const bool bIsRowHovered = IsHovered(); if (bIsSelected || bIsRowHovered) { return FStyleColors::White; } return FSlateColor::UseForeground(); } ) ] ] ] + SOverlay::Slot() [ SNew(SImage) .Visibility(EVisibility::HitTestInvisible) .Image_Lambda ( [this]() { const bool bIsSelected = IsSelected(); const bool bIsRowHovered = IsHovered(); if (bIsSelected && bIsRowHovered) { static const FName SelectedHover("ProjectBrowser.ProjectTile.SelectedHoverBorder"); return FAppStyle::Get().GetBrush(SelectedHover); } else if (bIsSelected) { static const FName Selected("ProjectBrowser.ProjectTile.SelectedBorder"); return FAppStyle::Get().GetBrush(Selected); } else if (bIsRowHovered) { static const FName Hovered("ProjectBrowser.ProjectTile.HoverBorder"); return FAppStyle::Get().GetBrush(Hovered); } return FStyleDefaults::GetNoBrush(); } ) ] ] ], OwnerTable ); } private: /** Get this item's thumbnail or return the default */ const FSlateBrush* GetThumbnail() const { TSharedPtr ItemPtr = Item.Pin(); if (ItemPtr.IsValid() && ItemPtr->Thumbnail.IsValid()) { return ItemPtr->Thumbnail.Get(); } return FAppStyle::GetBrush("UnrealDefaultThumbnail"); } }; /** * Simple widget used to display a folder path, and a name of a file */ class SFilepath : public SCompoundWidget { public: SLATE_BEGIN_ARGS( SFilepath ) : _IsReadOnly(false) {} /** Attribute specifying the text to display in the folder input */ SLATE_ATTRIBUTE(FText, FolderPath) /** Attribute specifying the text to display in the name input */ SLATE_ATTRIBUTE(FText, Name) SLATE_ATTRIBUTE(FText, WarningText) SLATE_ARGUMENT(bool, IsReadOnly) /** Event that is triggered when the browser for folder button is clicked */ SLATE_EVENT(FOnClicked, OnBrowseForFolder) /** Events for when the name field is manipulated */ SLATE_EVENT(FOnTextChanged, OnNameChanged) SLATE_EVENT(FOnTextCommitted, OnNameCommitted) /** Events for when the folder field is manipulated */ SLATE_EVENT(FOnTextChanged, OnFolderChanged) SLATE_EVENT(FOnTextCommitted, OnFolderCommitted) SLATE_END_ARGS() /** Constructs this widget with InArgs */ void Construct( const FArguments& InArgs ) { WarningText = InArgs._WarningText; ChildSlot [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .HAlign(HAlign_Left) .VAlign(VAlign_Top) .Padding(0.0f, 4.0f, 8.0f, 8.0f) .AutoWidth() [ SNew(STextBlock) .Text(LOCTEXT("ProjectLocation", "Project Location")) ] + SHorizontalBox::Slot() .VAlign(VAlign_Top) .AutoWidth() [ SNew(SBox) .WidthOverride(595.0f) [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() [ SNew(SEditableTextBox) .IsReadOnly(InArgs._IsReadOnly) .Text(InArgs._FolderPath) .OnTextChanged(InArgs._OnFolderChanged) .OnTextCommitted(InArgs._OnFolderCommitted) ] + SVerticalBox::Slot() .Padding(0.0f, 8.0f) [ SNew(SWarningOrErrorBox) .Visibility(this, &SFilepath::GetWarningVisibility) .IconSize(FVector2D(16,16)) .Padding(FMargin(8.0f, 4.0f, 4.0f, 4.0f)) .Message(WarningText) .MessageStyle(EMessageStyle::Error) ] ] ] + SHorizontalBox::Slot() .VAlign(VAlign_Top) .Padding(2.0f, 2.0f, 0.0f, 0.0f) .AutoWidth() [ SNew(SButton) .ButtonStyle(FAppStyle::Get(), "SimpleButton") .OnClicked(InArgs._OnBrowseForFolder) .ToolTipText(LOCTEXT("BrowseForFolder", "Browse for a folder")) .Visibility(InArgs._IsReadOnly ? EVisibility::Hidden : EVisibility::Visible) .ContentPadding(0.0f) [ SNew(SImage) .Image(FAppStyle::Get().GetBrush("Icons.FolderClosed")) .ColorAndOpacity(FSlateColor::UseForeground()) ] ] + SHorizontalBox::Slot() .HAlign(HAlign_Right) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .VAlign(VAlign_Top) .AutoWidth() .Padding(32.0f, 4.0f, 8.0f, 8.0f) [ SNew(STextBlock) .Text(LOCTEXT("ProjectName", "Project Name")) ] + SHorizontalBox::Slot() .VAlign(VAlign_Top) [ SNew(SBox) .WidthOverride(275.0f) [ SNew(SEditableTextBox) .IsReadOnly(InArgs._IsReadOnly) .Text(InArgs._Name) .OnTextChanged(InArgs._OnNameChanged) .OnTextCommitted(InArgs._OnNameCommitted) ] ] ] ]; } private: EVisibility GetWarningVisibility() const { return WarningText.Get().IsEmpty() ? EVisibility::Hidden : EVisibility::HitTestInvisible; } private: TAttribute WarningText; }; void SProjectDialog::Construct(const FArguments& InArgs, EProjectDialogModeMode Mode) { bLastGlobalValidityCheckSuccessful = true; bLastNameAndLocationValidityCheckSuccessful = true; PopulateTemplateCategories(); ProjectBrowser = SNew(SProjectBrowser); ChildSlot [ SNew(SBorder) .BorderImage(FAppStyle::Get().GetBrush("Brushes.Panel")) .Padding(FMargin(16.0f, 8.0f)) [ SNew(SVerticalBox) + SVerticalBox::Slot() .Padding(0.0f, 0.0f, 0.0f, 12.0f) [ MakeHybridView(Mode) ] +SVerticalBox::Slot() .AutoHeight() .Padding(-16.0f, 0.0f) [ SNew(SSeparator) .Orientation(EOrientation::Orient_Horizontal) .Thickness(2.0f) ] +SVerticalBox::Slot() .AutoHeight() [ SNew(SBox) .HeightOverride(145.0f) [ SAssignNew(PathAreaSwitcher, SWidgetSwitcher) +SWidgetSwitcher::Slot() [ MakeNewProjectPathArea() ] +SWidgetSwitcher::Slot() [ MakeOpenProjectPathArea() ] ] ] ] ]; RegisterActiveTimer(1.0f, FWidgetActiveTimerDelegate::CreateLambda( [this](double, float) { UpdateProjectFileValidity(); return EActiveTimerReturnType::Continue; })); if (Mode == EProjectDialogModeMode::OpenProject) { PathAreaSwitcher->SetActiveWidgetIndex(1); } else if (Mode == EProjectDialogModeMode::Hybrid && ProjectBrowser->HasProjects()) { // Select recent projects OnRecentProjectsClicked(); } else if(!ProjectBrowser->HasProjects() || Mode == EProjectDialogModeMode::NewProject) { // Select the first template category MajorCategoryList->SetSelection(TemplateCategories[0]); } } SProjectDialog::~SProjectDialog() { // remove any UTemplateProjectDefs we were keeping alive for (const TPair>>& Pair : Templates) { for (const TSharedPtr& Template : Pair.Value) { if (Template->CodeTemplateDefs != nullptr) { Template->CodeTemplateDefs->RemoveFromRoot(); } if (Template->BlueprintTemplateDefs != nullptr) { Template->BlueprintTemplateDefs->RemoveFromRoot(); } } } } void SProjectDialog::PopulateTemplateCategories() { TemplateCategories.Empty(); CurrentCategory.Reset(); TemplateCategories = GetAllTemplateCategories(); } TSharedRef SProjectDialog::MakeNewProjectDialogButtons() { return SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(8.0f, 0.0f, 8.0f, 0.0f) .VAlign(VAlign_Center) .AutoWidth() [ SNew(SGetSuggestedIDEWidget) .VisibilityOverride(this, &SProjectDialog::GetSuggestedIDEButtonVisibility) ] + SHorizontalBox::Slot() .Padding(8.0f, 0.0f, 8.0f, 0.0f) .VAlign(VAlign_Center) .AutoWidth() [ SNew(SGetDisableIDEWidget) .Visibility(this, &SProjectDialog::GetDisableIDEButtonVisibility) ] +SHorizontalBox::Slot() .Padding(8.0f, 0.0f, 8.0f, 0.0f) .VAlign(VAlign_Center) .AutoWidth() [ SNew(SPrimaryButton) .Visibility(this, &SProjectDialog::GetCreateButtonVisibility) .Text(LOCTEXT("CreateNewProject", "Create")) .IsEnabled(this, &SProjectDialog::CanCreateProject) .OnClicked_Lambda([this](){CreateAndOpenProject(); return FReply::Handled(); }) ] + SHorizontalBox::Slot() .Padding(8.0f, 0.0f, 0.0f, 0.0f) .VAlign(VAlign_Center) .AutoWidth() [ SNew(SButton) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .Text(LOCTEXT("CancelNewProjectCreation", "Cancel")) .OnClicked(this, &SProjectDialog::OnCancel) ]; } TSharedRef SProjectDialog::MakeOpenProjectDialogButtons() { TSharedRef ProjectBrowserRef = ProjectBrowser.ToSharedRef(); return SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(8.0f, 0.0f) .VAlign(VAlign_Center) .AutoWidth() [ SNew(SButton) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .Text(LOCTEXT("BrowseForProjects", "Browse...")) .ToolTipText(LOCTEXT("BrowseForProjects_Tooltip", "Browse to and open a project on your computer.")) .OnClicked(ProjectBrowserRef, &SProjectBrowser::OnBrowseToProject) ] +SHorizontalBox::Slot() .Padding(8.0f, 0.0f) .VAlign(VAlign_Center) .AutoWidth() [ SNew(SPrimaryButton) .Visibility(this, &SProjectDialog::GetCreateButtonVisibility) .Text(LOCTEXT("OpenProject", "Open")) .ToolTipText(LOCTEXT("OpenProject_Tooltip", "Open the selected project.")) .IsEnabled(ProjectBrowserRef, &SProjectBrowser::HasSelectedProjectFile) .OnClicked(ProjectBrowserRef, &SProjectBrowser::OnOpenProject) ] + SHorizontalBox::Slot() .Padding(8.0f, 0.0f, 0.0f, 0.0f) .VAlign(VAlign_Center) .AutoWidth() [ SNew(SButton) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .Text(LOCTEXT("CancelNewProjectCreation", "Cancel")) .OnClicked(this, &SProjectDialog::OnCancel) ]; } TSharedRef SProjectDialog::MakeTemplateProjectView() { return SNew(SVerticalBox) // Templates list + SVerticalBox::Slot() .FillHeight(1.0f) .Padding(0.0f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(0.0f, 0.0f, 0.0f, -12.0f) [ SNew(SScrollBorder, TemplateListView.ToSharedRef()) [ TemplateListView.ToSharedRef() ] ] + SHorizontalBox::Slot() .Padding(0, -8.0f, 0.0f, -12.0f) .AutoWidth() [ SNew(SSeparator) .Orientation(EOrientation::Orient_Vertical) .Thickness(2.0f) ] // Selected template details + SHorizontalBox::Slot() .Padding(8.0f, 0.0f) .AutoWidth() [ SNew(SVerticalBox) .Clipping(EWidgetClipping::ClipToBounds) // Preview image + SVerticalBox::Slot() .AutoHeight() .Padding(FMargin(0.0f, 0.0f, 0.0f, 15.f)) [ SNew(SImage) .DesiredSizeOverride(FVector2D(358,160)) .Image(this, &SProjectDialog::GetSelectedTemplatePreviewImage) ] // Template Name + SVerticalBox::Slot() .Padding(FMargin(0.0f, 0.0f, 0.0f, 10.0f)) .AutoHeight() [ SNew(STextBlock) .AutoWrapText(true) .TextStyle(FAppStyle::Get(), "DialogButtonText") .Font(FAppStyle::Get().GetFontStyle("HeadingExtraSmall")) .ColorAndOpacity(FAppStyle::Get().GetSlateColor("Colors.White")) .Text(this, &SProjectDialog::GetSelectedTemplateProperty, &FTemplateItem::Name) ] // Template Description + SVerticalBox::Slot() .AutoHeight() [ SNew(SBox) .HeightOverride(120.0f) .WidthOverride(358.0f) [ SNew(SScrollBox) + SScrollBox::Slot() [ SNew(SVerticalBox) +SVerticalBox::Slot() .Padding(FMargin(0.0f, 0.0f, 0.0f, 10.0f)) .AutoHeight() [ SNew(STextBlock) .WrapTextAt(350.0f) .Text(this, &SProjectDialog::GetSelectedTemplateProperty, &FTemplateItem::Description) ] // Asset types + SVerticalBox::Slot() .AutoHeight() .Padding(FMargin(0.0f, 5.0f, 0.0f, 5.0f)) [ SNew(SVerticalBox) .Visibility(this, &SProjectDialog::GetSelectedTemplateAssetVisibility) + SVerticalBox::Slot() [ SNew(STextBlock) .TextStyle(FAppStyle::Get(), "DialogButtonText") .Text(LOCTEXT("ProjectTemplateAssetTypes", "Asset Type References")) ] + SVerticalBox::Slot() .AutoHeight() [ SNew(STextBlock) .AutoWrapText(true) .Text(this, &SProjectDialog::GetSelectedTemplateAssetTypes) ] ] // Class types + SVerticalBox::Slot() .AutoHeight() .Padding(FMargin(0.0f, 5.0f, 0.0f, 5.0f)) [ SNew(SVerticalBox) .Visibility(this, &SProjectDialog::GetSelectedTemplateClassVisibility) + SVerticalBox::Slot() [ SNew(STextBlock) .TextStyle(FAppStyle::Get(), "DialogButtonText") .Text(LOCTEXT("ProjectTemplateClassTypes", "Class Type References")) ] + SVerticalBox::Slot() .AutoHeight() [ SNew(STextBlock) .AutoWrapText(true) .Text(this, &SProjectDialog::GetSelectedTemplateClassTypes) ] ] ] ] ] // Project Options + SVerticalBox::Slot() .Expose(ProjectOptionsSlot) ] ]; } TSharedRef SProjectDialog::MakeHybridView(EProjectDialogModeMode Mode) { SelectedHardwareClassTarget = EHardwareClass::Desktop; SelectedGraphicsPreset = EGraphicsPreset::Maximum; // Find all template projects Templates = FindTemplateProjects(); SetDefaultProjectLocation(); TemplateListView = SNew(STileView>) .ListItemsSource(&FilteredTemplateList) .SelectionMode(ESelectionMode::Single) .ClearSelectionOnClick(false) .ItemAlignment(EListItemAlignment::LeftAligned) .OnGenerateTile_Static(&STemplateTile::BuildTile) .ItemHeight(NewProjectDialogDefs::TemplateTileHeight+9) .ItemWidth(NewProjectDialogDefs::TemplateTileWidth+9) .OnSelectionChanged(this, &SProjectDialog::HandleTemplateListViewSelectionChanged); TSharedRef HybridView = SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ SNew(SVerticalBox) +SVerticalBox::Slot() .Padding(0.0f, 0.0f, 0.0f, 16.0f) .AutoHeight() [ SNew(SBox) .Visibility(Mode == EProjectDialogModeMode::Hybrid ? EVisibility::Visible : EVisibility::Collapsed) [ MakeRecentProjectsTile() ] ] + SVerticalBox::Slot() .Padding(0.0f, -4.0f, 0.0f, 0.0f) [ SNew(SBorder) .Visibility(Mode == EProjectDialogModeMode::OpenProject ? EVisibility::Collapsed : EVisibility::Visible) .BorderImage(FAppStyle::Get().GetBrush("ProjectBrowser.MajorCategoryViewBorder")) [ SAssignNew(MajorCategoryList, STileView>) .ListItemsSource(&TemplateCategories) .SelectionMode(ESelectionMode::Single) .ClearSelectionOnClick(false) .OnGenerateTile(this, &SProjectDialog::ConstructMajorCategoryTableRow) .ItemHeight(NewProjectDialogDefs::MajorItemHeight) .ItemWidth(NewProjectDialogDefs::MajorItemWidth) .OnSelectionChanged(this, &SProjectDialog::OnMajorTemplateCategorySelectionChanged) ] ] ] +SHorizontalBox::Slot() .Padding(11.0f, 0.0f, 0.0f, 0.0f) [ SAssignNew(TemplateAndRecentProjectsSwitcher, SWidgetSwitcher) + SWidgetSwitcher::Slot() [ MakeTemplateProjectView() ] + SWidgetSwitcher::Slot() [ ProjectBrowser.ToSharedRef() ] ]; SetCurrentMajorCategory(ActiveCategory); if (Mode == EProjectDialogModeMode::OpenProject) { TemplateAndRecentProjectsSwitcher->SetActiveWidgetIndex(1); } UpdateProjectFileValidity(); return HybridView; } TSharedRef SProjectDialog::MakeProjectOptionsWidget() { IHardwareTargetingModule& HardwareTargeting = IHardwareTargetingModule::Get(); TemplateVariantNames.Empty(); SelectedVariantName = NAME_None; const UTemplateProjectDefs* ProjectDefs = GetSelectedTemplateDefs(); TSharedPtr ProjectOptionsBox; TSharedRef ProjectOptionsWidget = SNew(SVerticalBox) .Visibility(this, &SProjectDialog::GetProjectSettingsVisibility) + SVerticalBox::Slot() .AutoHeight() [ SNew(SBorder) .Padding(FMargin(10.0f, 7.0f)) .BorderImage(FAppStyle::Get().GetBrush("Brushes.Header")) [ SNew(STextBlock) .TextStyle(FAppStyle::Get(), "DialogButtonText") .Text(LOCTEXT("ProjectDefaults", "Project Defaults")) ] ] + SVerticalBox::Slot() .Padding(0.0f, 20.0f, 0.0f, 0.0f) [ SNew(SScrollBox) + SScrollBox::Slot() [ SAssignNew(ProjectOptionsBox, SVerticalBox) ] ]; const TArray& HiddenSettings = GetSelectedTemplateProperty(&FTemplateItem::HiddenSettings); bool bIsBlueprintAvailable = !GetSelectedTemplateProperty(&FTemplateItem::BlueprintProjectFile).IsEmpty(); bool bIsCodeAvailable = !GetSelectedTemplateProperty(&FTemplateItem::CodeProjectFile).IsEmpty(); if (!HiddenSettings.Contains(ETemplateSetting::Languages)) { // if neither is available, then this is a blank template, so both are available if (!bIsBlueprintAvailable && !bIsCodeAvailable) { bIsBlueprintAvailable = true; bIsCodeAvailable = true; } bShouldGenerateCode = !bIsBlueprintAvailable; TSharedRef> ScriptTypeChooser = SNew(SSegmentedControl) .UniformPadding(FMargin(25.0f,4.0f)) .ToolTipText(LOCTEXT("ProjectDialog_BlueprintOrCppDescription", "Choose whether to create a Blueprint or C++ project.\nNote: You can also add blueprints to a C++ project and C++ to a Blueprint project later.")) .Value(this, &SProjectDialog::OnGetBlueprintOrCppIndex) .OnValueChanged(this, &SProjectDialog::OnSetBlueprintOrCppIndex); if (bIsBlueprintAvailable) { ScriptTypeChooser->AddSlot(0) .HAlign(HAlign_Center) .Text(LOCTEXT("ProjectDialog_Blueprint", "BLUEPRINT")); } if (bIsCodeAvailable) { ScriptTypeChooser->AddSlot(1) .HAlign(HAlign_Center) .Text(LOCTEXT("ProjectDialog_Code", "C++")); } ProjectOptionsBox->AddSlot() .AutoHeight() .HAlign(HAlign_Center) [ ScriptTypeChooser ]; } else { bShouldGenerateCode = bIsCodeAvailable; } if (!HiddenSettings.Contains(ETemplateSetting::HardwareTarget)) { TSharedRef HardwareClassTarget = HardwareTargeting.MakeHardwareClassTargetCombo( FOnHardwareClassChanged::CreateSP(this, &SProjectDialog::SetHardwareClassTarget), TAttribute(this, &SProjectDialog::GetHardwareClassTarget)); ProjectOptionsBox->AddSlot() .Padding(0.0f, 16.0f, 0.0f, 8.0f) .AutoHeight() [ SNew(SHorizontalBox) .ToolTipText(LOCTEXT("ProjectDialog_HardwareClassTargetDescription", "Choose the closest equivalent target platform. You can change this later in the Target Hardware section of Project Settings.")) + SHorizontalBox::Slot() .Padding(0.0f, 0.0f, 8.0f, 0.0f) .VAlign(VAlign_Center) .HAlign(HAlign_Right) [ SNew(STextBlock) .Text(LOCTEXT("TargetPlatform", "Target Platform")) ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .HAlign(HAlign_Fill) [ SNew(SBox) [ HardwareClassTarget ] ] ]; } if (!HiddenSettings.Contains(ETemplateSetting::GraphicsPreset)) { TSharedRef GraphicsPreset = HardwareTargeting.MakeGraphicsPresetTargetCombo( FOnGraphicsPresetChanged::CreateSP(this, &SProjectDialog::SetGraphicsPreset), TAttribute(this, &SProjectDialog::GetGraphicsPreset)); ProjectOptionsBox->AddSlot() .AutoHeight() [ SNew(SHorizontalBox) .ToolTipText(LOCTEXT("ProjectDialog_GraphicsPresetDescription", "Choose the performance characteristics of your project. You can change this later in the Target Hardware section of Project Settings.")) + SHorizontalBox::Slot() .Padding(0.0f, 0.0f, 8.0f, 0.0f) .VAlign(VAlign_Center) .HAlign(HAlign_Right) [ SNew(STextBlock) .Text(LOCTEXT("QualityPreset", "Quality Preset")) ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .HAlign(HAlign_Fill) [ SNew(SBox) [ GraphicsPreset ] ] ]; } if (!HiddenSettings.Contains(ETemplateSetting::Variants) && ProjectDefs) { TemplateVariantNames.Add(NAME_None); // The user can choose to have no variants Algo::Transform(ProjectDefs->Variants, TemplateVariantNames, [](const FTemplateVariant& Variant) { return Variant.Name; }); ProjectOptionsBox->AddSlot() .Padding(0.0f, 8.0f, 0.0f, 0.0f) .AutoHeight() [ SNew(SHorizontalBox) .ToolTipText(LOCTEXT("Variants_ToolTip", "Which variant would you like to use?")) .Visibility(this, &SProjectDialog::GetVariantsVisibility) + SHorizontalBox::Slot() .Padding(0.0f, 0.0f, 8.0f, 0.0f) .VAlign(VAlign_Center) .HAlign(HAlign_Right) [ SNew(STextBlock) .Text(LOCTEXT("Variant", "Variant")) ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .HAlign(HAlign_Fill) [ SNew(SComboButton) .ContentPadding(FMargin(4.0f, 0.0f)) .OnGetMenuContent(this, &SProjectDialog::GetVariantsDropdownContent) .ButtonContent() [ SNew(STextBlock) .Text(this, &SProjectDialog::GetVariantsButtonText) .ToolTipText(this, &SProjectDialog::GetVariantsButtonTooltip) ] ] ]; } #if 0 // @todo: XR settings cannot be shown at the moment as the setting causes issues with binary builds. if (!HiddenSettings.Contains(ETemplateSetting::XR)) { TArray::FComboOption> VirtualRealityOptions; VirtualRealityOptions.Add(SDecoratedEnumCombo::FComboOption( 0, FSlateIcon(FAppStyle::GetAppStyleSetName(), "GameProjectDialog.XRDisabled"), LOCTEXT("XRDisabled", "XR Disabled"))); VirtualRealityOptions.Add(SDecoratedEnumCombo::FComboOption( 1, FSlateIcon(FAppStyle::GetAppStyleSetName(), "GameProjectDialog.XREnabled"), LOCTEXT("XREnabled", "XR Enabled"))); TSharedRef> Enum = SNew(SDecoratedEnumCombo, MoveTemp(VirtualRealityOptions)) .SelectedEnum(this, &SProjectDialog::OnGetXREnabled) .OnEnumChanged(this, &SProjectDialog::OnSetXREnabled) .Orientation(Orient_Vertical); TSharedRef Description = SNew(SRichTextBlock) .Text(LOCTEXT("ProjectDialog_XREnabledDescription", "Choose if XR should be enabled in the new project.")) .AutoWrapText(true) .DecoratorStyleSet(&FAppStyle::Get()); } #endif return ProjectOptionsWidget; } TSharedRef SProjectDialog::MakeRecentProjectsTile() { RecentProjectsCategory = MakeShared(); RecentProjectsCategory->DisplayName = LOCTEXT("RecentProjects", "Recent Projects"); RecentProjectsCategory->Description = FText::GetEmpty(); RecentProjectsCategory->Key = "RecentProjects"; RecentProjectsCategory->IsEnterprise = false; static const FName BrushName = *(FAppStyle::Get().GetContentRootDir() / TEXT("/Starship/Projects/") / TEXT("RecentProjects_2x.png")); RecentProjectsBrush = MakeUnique(BrushName, FVector2D(300, 100)); RecentProjectsBrush->OutlineSettings.CornerRadii = FVector4(4, 4, 4, 4); RecentProjectsBrush->OutlineSettings.RoundingType = ESlateBrushRoundingType::FixedRadius; RecentProjectsBrush->DrawAs = ESlateBrushDrawType::RoundedBox; RecentProjectsCategory->Icon = RecentProjectsBrush.Get(); return SNew(SButton) .ButtonStyle(FAppStyle::Get(), "InvisibleButton") .OnClicked(this, &SProjectDialog::OnRecentProjectsClicked) .ForegroundColor(FLinearColor::White) .ContentPadding(FMargin(4.0f, 0.0f)) [ SNew(SMajorCategoryTile, RecentProjectsCategory) .IsSelected_Lambda([this]() { return RecentProjectsCategory == CurrentCategory; }) ]; } TSharedRef SProjectDialog::MakeNewProjectPathArea() { return SNew(SVerticalBox) +SVerticalBox::Slot() .Padding(25.0f, 36.0f, 0.0f, 0) [ SNew(SFilepath) .OnBrowseForFolder(this, &SProjectDialog::HandlePathBrowseButtonClicked) .FolderPath(this, &SProjectDialog::GetCurrentProjectFilePath) .WarningText(this, &SProjectDialog::GetNameAndLocationValidityErrorText) .Name(this, &SProjectDialog::GetCurrentProjectFileName) .OnFolderChanged(this, &SProjectDialog::OnCurrentProjectFilePathChanged) .OnNameChanged(this, &SProjectDialog::OnCurrentProjectFileNameChanged) ] +SVerticalBox::Slot() .Padding(0.0f, 0.0f, 0.0f, 8.0f) .VAlign(VAlign_Bottom) .HAlign(HAlign_Right) .AutoHeight() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() [ SNew(SWarningOrErrorBox) .Padding(FMargin(8.0f, 4.0f, 4.0f, 4.0f)) .IconSize(FVector2D(16,16)) .MessageStyle(EMessageStyle::Error) .Message(this, &SProjectDialog::GetGlobalErrorLabelText) .Visibility(this, &SProjectDialog::GetGlobalErrorVisibility) ] +SHorizontalBox::Slot() .VAlign(VAlign_Bottom) [ MakeNewProjectDialogButtons() ] ]; } TSharedRef SProjectDialog::MakeOpenProjectPathArea() { return SNew(SVerticalBox) + SVerticalBox::Slot() .Padding(25.0f, 36.0f, 0.0f, 0) [ SNew(SFilepath) .IsReadOnly(true) .FolderPath(this, &SProjectDialog::GetCurrentProjectFilePath) .Name(this, &SProjectDialog::GetCurrentProjectFileName) ] +SVerticalBox::Slot() .Padding(25.0f, 0.0f, 0.0f, 8.0f) .VAlign(VAlign_Bottom) .AutoHeight() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() [ SNew(SCheckBox) .IsChecked(GetDefault()->bLoadTheMostRecentlyLoadedProjectAtStartup ? ECheckBoxState::Checked : ECheckBoxState::Unchecked) .OnCheckStateChanged(ProjectBrowser.ToSharedRef(), &SProjectBrowser::OnAutoloadLastProjectChanged) .Padding(FMargin(4.0f, 0.0f)) [ SNew(STextBlock) .Text(LOCTEXT("AutoloadOnStartupCheckbox", "Always load last project on startup")) .ColorAndOpacity(FSlateColor::UseForeground()) ] ] + SHorizontalBox::Slot() .VAlign(VAlign_Bottom) .HAlign(HAlign_Right) [ MakeOpenProjectDialogButtons() ] ]; } bool SProjectDialog::CanCreateProject() const { return bLastGlobalValidityCheckSuccessful && bLastNameAndLocationValidityCheckSuccessful; } FReply SProjectDialog::OnCancel() const { TSharedPtr Window = FSlateApplication::Get().FindWidgetWindow(AsShared()); Window->RequestDestroyWindow(); return FReply::Handled(); } void SProjectDialog::OnSetBlueprintOrCppIndex(int32 Index) { bShouldGenerateCode = Index == 1; UpdateProjectFileValidity(); FProjectInformation ProjectInfo = CreateProjectInfo(); // Recreate the VariantNames as BP and CPP templates can have different variants const UTemplateProjectDefs* ProjectDefs = GetSelectedTemplateDefs(); TemplateVariantNames.Empty(); if (ProjectDefs) { Algo::Transform(ProjectDefs->Variants, TemplateVariantNames, [](const FTemplateVariant& Variant) { return Variant.Name; }); } // Check if this template has a variant of the same name, otherwise resets it const bool bFoundVariant = ProjectDefs && ProjectDefs->FindVariant(SelectedVariantName); if (!bFoundVariant) { SelectedVariantName = NAME_None; } } void SProjectDialog::SetHardwareClassTarget(EHardwareClass InHardwareClass) { SelectedHardwareClassTarget = InHardwareClass; } void SProjectDialog::SetGraphicsPreset(EGraphicsPreset InGraphicsPreset) { SelectedGraphicsPreset = InGraphicsPreset; } EVisibility SProjectDialog::GetVariantsVisibility() const { const UTemplateProjectDefs* ProjectDefs = GetSelectedTemplateDefs(); return ProjectDefs && !ProjectDefs->Variants.IsEmpty() ? EVisibility::Visible : EVisibility::Collapsed; } TSharedRef SProjectDialog::GetVariantsDropdownContent() { FMenuBuilder MenuBuilder(true, nullptr, nullptr, true); for (const FName& VariantName : TemplateVariantNames) { const FTemplateVariant* Variant = GetSelectedTemplateDefsVariant(VariantName); MenuBuilder.AddMenuEntry( Variant ? Variant->GetDisplayNameText() : LOCTEXT("NoVariant", "None"), Variant ? Variant->GetLocalizedDescription() : LOCTEXT("NoVariantDescription", "Do not include a variant"), FSlateIcon(), FUIAction ( FExecuteAction::CreateLambda([this, VariantName]() { SelectedVariantName = VariantName; }) ) ); } return MenuBuilder.MakeWidget(); } FText SProjectDialog::GetVariantsButtonText() const { const FTemplateVariant* Variant = GetSelectedTemplateDefsVariant(); return Variant ? Variant->GetDisplayNameText() : LOCTEXT("NoVariant", "None"); } FText SProjectDialog::GetVariantsButtonTooltip() const { const FTemplateVariant* Variant = GetSelectedTemplateDefsVariant(); return Variant ? Variant->GetLocalizedDescription() : LOCTEXT("NoVariantDescription", "Do not include a variant"); } void SProjectDialog::HandleTemplateListViewSelectionChanged(TSharedPtr TemplateItem, ESelectInfo::Type SelectInfo) { (*ProjectOptionsSlot) [ MakeProjectOptionsWidget() ]; UpdateProjectFileValidity(); } TSharedPtr SProjectDialog::GetSelectedTemplateItem() const { TArray> SelectedItems = TemplateListView->GetSelectedItems(); if (SelectedItems.Num() > 0) { return SelectedItems[0]; } return nullptr; } UTemplateProjectDefs* SProjectDialog::GetSelectedTemplateDefs() const { if (const TSharedPtr TemplateItem = GetSelectedTemplateItem()) { return bShouldGenerateCode ? TemplateItem->CodeTemplateDefs : TemplateItem->BlueprintTemplateDefs; } return nullptr; } const FTemplateVariant* SProjectDialog::GetSelectedTemplateDefsVariant(FName VariantName) const { const UTemplateProjectDefs* ProjectDefs = GetSelectedTemplateDefs(); return ProjectDefs ? ProjectDefs->FindVariant(VariantName) : nullptr; } namespace { FString MakeSortKey(const FString& TemplateKey) { FString Output = TemplateKey; #if PLATFORM_LINUX // Paths with a leading "/" would get sorted before the magic value used for blank projects: "_1" Output.RemoveFromStart("/"); #endif return Output; } } TMap> > SProjectDialog::FindTemplateProjects() { // Clear the list out first - or we could end up with duplicates TMap>> Templates; // Now discover and all data driven templates TArray TemplateRootFolders; // @todo rocket make template folder locations extensible. TemplateRootFolders.Add(FPaths::RootDir() + TEXT("Templates")); // Add the Enterprise templates TemplateRootFolders.Add(FPaths::EnterpriseDir() + TEXT("Templates")); // Allow plugins to define templates TArray> Plugins = IPluginManager::Get().GetEnabledPlugins(); for (const TSharedRef& Plugin : Plugins) { FString PluginDirectory = Plugin->GetBaseDir(); if (!PluginDirectory.IsEmpty()) { const FString PluginTemplatesDirectory = FPaths::Combine(*PluginDirectory, TEXT("Templates")); if (IFileManager::Get().DirectoryExists(*PluginTemplatesDirectory)) { TemplateRootFolders.Add(PluginTemplatesDirectory); } } } // Form a list of all folders that could contain template projects TArray AllTemplateFolders; for (const FString& Root : TemplateRootFolders) { const FString SearchString = Root / TEXT("*"); TArray TemplateFolders; IFileManager::Get().FindFiles(TemplateFolders, *SearchString, /*Files=*/false, /*Directories=*/true); for (const FString& Folder : TemplateFolders) { AllTemplateFolders.Add(Root / Folder); } } TArray> FoundTemplates; // Add a template item for every discovered project for (const FString& Root : AllTemplateFolders) { const FString SearchString = Root / TEXT("*.") + FProjectDescriptor::GetExtension(); TArray FoundProjectFiles; IFileManager::Get().FindFiles(FoundProjectFiles, *SearchString, /*Files=*/true, /*Directories=*/false); if (FoundProjectFiles.Num() == 0 || !ensure(FoundProjectFiles.Num() == 1)) { continue; } // Make sure a TemplateDefs.ini file exists UTemplateProjectDefs* TemplateDefs = GameProjectUtils::LoadTemplateDefs(Root); if (TemplateDefs == nullptr) { continue; } // we don't have an appropriate referencing UObject to keep these alive with, so we need to keep these template defs alive from GC TemplateDefs->AddToRoot(); // Ignore any templates whose definition says we cannot use to create a project if (TemplateDefs->bAllowProjectCreation == false) { continue; } const FString ProjectFile = Root / FoundProjectFiles[0]; // If no template category was specified, use the default category TArray TemplateCategoryNames = TemplateDefs->Categories; if (TemplateCategoryNames.Num() == 0) { TemplateCategoryNames.Add(NewProjectDialogDefs::DefaultCategoryName); } // Find a duplicate project, eg. "Foo" and "FooBP" FString TemplateKey = Root; TemplateKey.RemoveFromEnd("BP"); TSharedPtr* ExistingTemplate = FoundTemplates.FindByPredicate([&TemplateKey](TSharedPtr Item) { return Item->Key == TemplateKey; }); TSharedPtr Template; // Create a new template if none was found if (ExistingTemplate != nullptr) { Template = *ExistingTemplate; } else { Template = MakeShareable(new FTemplateItem()); } if (TemplateDefs->GeneratesCode(Root)) { Template->CodeProjectFile = ProjectFile; Template->CodeTemplateDefs = TemplateDefs; } else { Template->BlueprintProjectFile = ProjectFile; Template->BlueprintTemplateDefs = TemplateDefs; } // The rest has already been set by the existing template, so skip it. if (ExistingTemplate != nullptr) { continue; } // Did not find an existing template. Create a new one to add to the template list. Template->Key = TemplateKey; // @todo: These are all basically just copies of what's in UTemplateProjectDefs, but ignore differences between code and BP Template->Categories = TemplateCategoryNames; Template->Description = TemplateDefs->GetLocalizedDescription(); Template->ClassTypes = TemplateDefs->ClassTypes; Template->AssetTypes = TemplateDefs->AssetTypes; Template->HiddenSettings = TemplateDefs->HiddenSettings; Template->bIsEnterprise = TemplateDefs->bIsEnterprise; Template->bIsBlankTemplate = TemplateDefs->bIsBlank; Template->bThumbnailAsIcon = TemplateDefs->bThumbnailAsIcon; Template->Name = TemplateDefs->GetDisplayNameText(); if (Template->Name.IsEmpty()) { Template->Name = FText::FromString(TemplateKey); } const FString ThumbnailPNGFile = (Root + TEXT("/Media/") + FoundProjectFiles[0]).Replace(TEXT(".uproject"), TEXT(".png")); if (FPlatformFileManager::Get().GetPlatformFile().FileExists(*ThumbnailPNGFile)) { const FName BrushName = FName(*ThumbnailPNGFile); Template->Thumbnail = MakeShareable(new FSlateDynamicImageBrush(BrushName, FVector2D(128, 128))); } TSharedPtr PreviewBrush; const FString PreviewPNGFile = (Root + TEXT("/Media/") + FoundProjectFiles[0]).Replace(TEXT(".uproject"), TEXT("_Preview.png")); if (FPlatformFileManager::Get().GetPlatformFile().FileExists(*PreviewPNGFile)) { const FName BrushName = FName(*PreviewPNGFile); Template->PreviewImage = MakeShareable(new FSlateDynamicImageBrush(BrushName, FVector2D(512, 256))); } Template->SortKey = TemplateDefs->SortKey; if (Template->SortKey.IsEmpty()) { Template->SortKey = MakeSortKey(TemplateKey); } FoundTemplates.Add(Template); } for (const TSharedPtr& Template : FoundTemplates) { for (const FName& Category : Template->Categories) { Templates.FindOrAdd(Category).Add(Template); } } TArray> AllTemplateCategories = GetAllTemplateCategories(); // Validate that all our templates have a category defined TArray CategoryKeys; Templates.GetKeys(CategoryKeys); for (const FName& CategoryKey : CategoryKeys) { bool bCategoryExists = AllTemplateCategories.ContainsByPredicate([&CategoryKey](const TSharedPtr& Category) { return Category->Key == CategoryKey; }); if (!bCategoryExists) { UE_LOG(LogGameProjectGeneration, Warning, TEXT("Failed to find category definition named '%s', it is not defined in any TemplateCategories.ini."), *CategoryKey.ToString()); } } // Add blank template to empty categories { TSharedPtr BlankTemplate = MakeShareable(new FTemplateItem()); BlankTemplate->Name = LOCTEXT("BlankProjectName", "Blank"); BlankTemplate->Description = LOCTEXT("BlankProjectDescription", "A clean empty project with no code and default settings."); BlankTemplate->Key = TEXT("Blank"); BlankTemplate->SortKey = TEXT("_1"); BlankTemplate->Thumbnail = MakeShareable(new FSlateBrush(*FAppStyle::GetBrush("GameProjectDialog.BlankProjectThumbnail"))); BlankTemplate->PreviewImage = MakeShareable(new FSlateBrush(*FAppStyle::GetBrush("GameProjectDialog.BlankProjectPreview"))); BlankTemplate->BlueprintProjectFile = TEXT(""); BlankTemplate->CodeProjectFile = TEXT(""); BlankTemplate->bIsEnterprise = false; BlankTemplate->bIsBlankTemplate = true; for (const TSharedPtr& Category : AllTemplateCategories) { const TArray>* CategoryEntry = Templates.Find(Category->Key); if (CategoryEntry == nullptr) { Templates.Add(Category->Key).Add(BlankTemplate); } } } return Templates; } void SProjectDialog::SetDefaultProjectLocation() { FString DefaultProjectFilePath; // First, try and use the first previously used path that still exists for (const FString& CreatedProjectPath : GetDefault()->CreatedProjectPaths) { if (IFileManager::Get().DirectoryExists(*CreatedProjectPath)) { DefaultProjectFilePath = CreatedProjectPath; break; } } if (DefaultProjectFilePath.IsEmpty()) { // No previously used path, decide a default path. DefaultProjectFilePath = FDesktopPlatformModule::Get()->GetDefaultProjectCreationPath(); IFileManager::Get().MakeDirectory(*DefaultProjectFilePath, true); } if (DefaultProjectFilePath.EndsWith(TEXT("/"))) { DefaultProjectFilePath.LeftChopInline(1); } FPaths::NormalizeFilename(DefaultProjectFilePath); FPaths::MakePlatformFilename(DefaultProjectFilePath); const FString GenericProjectName = LOCTEXT("DefaultProjectName", "MyProject").ToString(); FString ProjectName = GenericProjectName; // Check to make sure the project file doesn't already exist FText FailReason; if (!GameProjectUtils::IsValidProjectFileForCreation(DefaultProjectFilePath / ProjectName / ProjectName + TEXT(".") + FProjectDescriptor::GetExtension(), FailReason)) { // If it exists, find an appropriate numerical suffix const int MaxSuffix = 1000; int32 Suffix; for (Suffix = 2; Suffix < MaxSuffix; ++Suffix) { ProjectName = GenericProjectName + FString::Printf(TEXT("%d"), Suffix); if (GameProjectUtils::IsValidProjectFileForCreation(DefaultProjectFilePath / ProjectName / ProjectName + TEXT(".") + FProjectDescriptor::GetExtension(), FailReason)) { // Found a name that is not taken. Break out. break; } } if (Suffix >= MaxSuffix) { UE_LOG(LogGameProjectGeneration, Warning, TEXT("Failed to find a suffix for the default project name")); ProjectName = TEXT(""); } } if (!DefaultProjectFilePath.IsEmpty()) { CurrentProjectFileName = ProjectName; CurrentProjectFilePath = DefaultProjectFilePath; FPaths::MakePlatformFilename(CurrentProjectFilePath); LastBrowsePath = CurrentProjectFilePath; } } void SProjectDialog::SetCurrentMajorCategory(FName Category) { FilteredTemplateList = Templates.FindRef(Category); // Sort the template folders FilteredTemplateList.Sort([](const TSharedPtr& A, const TSharedPtr& B) { return A->SortKey < B->SortKey; }); if (FilteredTemplateList.Num() > 0) { TemplateListView->SetSelection(FilteredTemplateList[0]); } TemplateListView->RequestListRefresh(); ActiveCategory = Category; } FReply SProjectDialog::OnRecentProjectsClicked() { CurrentCategory = RecentProjectsCategory; ActiveCategory = NAME_None; MajorCategoryList->ClearSelection(); TemplateAndRecentProjectsSwitcher->SetActiveWidgetIndex(1); PathAreaSwitcher->SetActiveWidgetIndex(1); return FReply::Handled(); } FProjectInformation SProjectDialog::CreateProjectInfo() const { TSharedPtr SelectedTemplate = GetSelectedTemplateItem(); if (!SelectedTemplate.IsValid()) { return FProjectInformation(); } FProjectInformation ProjectInfo; ProjectInfo.bShouldGenerateCode = bShouldGenerateCode; ProjectInfo.TemplateFile = bShouldGenerateCode ? SelectedTemplate->CodeProjectFile : SelectedTemplate->BlueprintProjectFile; ProjectInfo.Variant = SelectedVariantName; ProjectInfo.TemplateCategory = ActiveCategory; ProjectInfo.bIsEnterpriseProject = SelectedTemplate->bIsEnterprise; ProjectInfo.bIsBlankTemplate = SelectedTemplate->bIsBlankTemplate; const TArray& HiddenSettings = SelectedTemplate->HiddenSettings; if (!HiddenSettings.Contains(ETemplateSetting::All)) { if (!HiddenSettings.Contains(ETemplateSetting::HardwareTarget)) { ProjectInfo.TargetedHardware = SelectedHardwareClassTarget; } if (!HiddenSettings.Contains(ETemplateSetting::GraphicsPreset)) { ProjectInfo.DefaultGraphicsPerformance = SelectedGraphicsPreset; } if (!HiddenSettings.Contains(ETemplateSetting::XR)) { ProjectInfo.bEnableXR = bEnableXR; } } return MoveTemp(ProjectInfo); } bool SProjectDialog::CreateProject(const FString& ProjectFile) { // Get the selected template TSharedPtr SelectedTemplate = GetSelectedTemplateItem(); if (!ensure(SelectedTemplate.IsValid())) { // A template must be selected. return false; } FText FailReason, FailLog; FProjectInformation ProjectInfo = CreateProjectInfo(); ProjectInfo.ProjectFilename = ProjectFile; if (!GameProjectUtils::CreateProject(ProjectInfo, FailReason, FailLog)) { SOutputLogDialog::Open(LOCTEXT("CreateProject", "Create Project"), FailReason, FailLog, FText::GetEmpty()); return false; } // Successfully created the project. Update the last created location string. FString CreatedProjectPath = FPaths::GetPath(FPaths::GetPath(ProjectFile)); // If the original path was the drives root (ie: C:/) the double path call strips the last / if (CreatedProjectPath.EndsWith(":")) { CreatedProjectPath.AppendChar('/'); } UEditorSettings* Settings = GetMutableDefault(); Settings->CreatedProjectPaths.Remove(CreatedProjectPath); Settings->CreatedProjectPaths.Insert(CreatedProjectPath, 0); Settings->PostEditChange(); return true; } void SProjectDialog::CreateAndOpenProject() { if (!CanCreateProject()) { return; } FString ProjectFile = GetProjectFilenameWithPath(); if (!CreateProject(ProjectFile)) { return; } if (bShouldGenerateCode) { // If the engine is installed it is already compiled, so we can try to build and open a new project immediately. Non-installed situations might require building // the engine (especially the case when binaries came from P4), so we only open the IDE for that. if (FApp::IsEngineInstalled()) { if (GameProjectUtils::BuildCodeProject(ProjectFile)) { OpenCodeIDE(ProjectFile); OpenProject(ProjectFile); } else { // User will have already been prompted to open the IDE } } else { OpenCodeIDE(ProjectFile); } } else { OpenProject(ProjectFile); } } bool SProjectDialog::OpenProject(const FString& ProjectFile) { FText FailReason; if (GameProjectUtils::OpenProject(ProjectFile, FailReason)) { // Successfully opened the project, the editor is closing. // Close this window in case something prevents the editor from closing (save dialog, quit confirmation, etc) CloseWindowIfAppropriate(); return true; } DisplayError(FailReason); return false; } void SProjectDialog::CloseWindowIfAppropriate(bool ForceClose) { if (ForceClose || FApp::HasProjectName()) { TSharedPtr ContainingWindow = FSlateApplication::Get().FindWidgetWindow(AsShared()); if (ContainingWindow.IsValid()) { ContainingWindow->RequestDestroyWindow(); } } } void SProjectDialog::DisplayError(const FText& ErrorText) { FString ErrorString = ErrorText.ToString(); UE_LOG(LogGameProjectGeneration, Log, TEXT("%s"), *ErrorString); if(ErrorString.Contains("\n")) { FMessageDialog::Open(EAppMsgType::Ok, ErrorText); } else { PersistentGlobalErrorLabelText = ErrorText; } } bool SProjectDialog::OpenCodeIDE(const FString& ProjectFile) { FText FailReason; #if PLATFORM_MAC // Modern Xcode projects are different based on Desktop/Mobile FString Extension = FPaths::GetExtension(ProjectFile); FString ModernXcodeProjectFile = ProjectFile; if (SelectedHardwareClassTarget == EHardwareClass::Desktop) { ModernXcodeProjectFile.RemoveFromEnd(TEXT(".") + Extension); ModernXcodeProjectFile += TEXT(" (Mac).") + Extension; } else if (SelectedHardwareClassTarget == EHardwareClass::Mobile) { ModernXcodeProjectFile.RemoveFromEnd(TEXT(".") + Extension); ModernXcodeProjectFile += TEXT(" (IOS).") + Extension; } #endif if ( #if PLATFORM_MAC GameProjectUtils::OpenCodeIDE(ModernXcodeProjectFile, FailReason) || // if modern failed, try again with legacy project name #endif GameProjectUtils::OpenCodeIDE(ProjectFile, FailReason)) { // Successfully opened code editing IDE, the editor is closing // Close this window in case something prevents the editor from closing (save dialog, quit confirmation, etc) CloseWindowIfAppropriate(true); return true; } DisplayError(FailReason); return false; } TArray> SProjectDialog::GetAllTemplateCategories() { TArray> AllTemplateCategories; FGameProjectGenerationModule::Get().GetAllTemplateCategories(AllTemplateCategories); if (AllTemplateCategories.Num() == 0) { static const FName BrushName = *(FAppStyle::Get().GetContentRootDir() / TEXT("/Starship/Projects/") / TEXT("CustomTemplate_2x.png")); if (!CustomTemplateBrush) { CustomTemplateBrush = MakeUnique(BrushName, FVector2D(300, 100)); CustomTemplateBrush->OutlineSettings.CornerRadii = FVector4(4, 4, 4, 4); CustomTemplateBrush->OutlineSettings.RoundingType = ESlateBrushRoundingType::FixedRadius; CustomTemplateBrush->DrawAs = ESlateBrushDrawType::RoundedBox; } TSharedPtr DefaultCategory = MakeShared(); DefaultCategory->Key = NewProjectDialogDefs::BlankCategoryKey; DefaultCategory->DisplayName = LOCTEXT("ProjectDialog_DefaultCategoryName", "Blank Project"); DefaultCategory->Description = LOCTEXT("ProjectDialog_DefaultCategoryDescription", "Create a new blank Unreal project."); DefaultCategory->Icon = CustomTemplateBrush.Get(); AllTemplateCategories.Add(DefaultCategory); } return AllTemplateCategories; } FText SProjectDialog::GetGlobalErrorLabelText() const { if (!PersistentGlobalErrorLabelText.IsEmpty()) { return PersistentGlobalErrorLabelText; } if (!bLastGlobalValidityCheckSuccessful) { return LastGlobalValidityErrorText; } return FText::GetEmpty(); } FText SProjectDialog::GetNameAndLocationValidityErrorText() const { if (GetGlobalErrorLabelText().IsEmpty()) { return bLastNameAndLocationValidityCheckSuccessful == false ? LastNameAndLocationValidityErrorText : FText::GetEmpty(); } return FText::GetEmpty(); } EVisibility SProjectDialog::GetCreateButtonVisibility() const { return IsCompilerRequired() && !FSourceCodeNavigation::IsCompilerAvailable() ? EVisibility::Collapsed : EVisibility::Visible; } EVisibility SProjectDialog::GetSuggestedIDEButtonVisibility() const { return IsCompilerRequired() && !FSourceCodeNavigation::IsCompilerAvailable() ? EVisibility::Visible : EVisibility::Collapsed; } // Allow disabling of the current IDE for platforms that dont require an IDE to run the Editor/Engine EVisibility SProjectDialog::GetDisableIDEButtonVisibility() const { if (GetSuggestedIDEButtonVisibility() == EVisibility::Visible && !IsIDERequired()) { return EVisibility::Visible; } return EVisibility::Collapsed; } const FSlateBrush* SProjectDialog::GetSelectedTemplatePreviewImage() const { TSharedPtr PreviewImage = GetSelectedTemplateProperty(&FTemplateItem::PreviewImage); return PreviewImage.IsValid() ? PreviewImage.Get() : nullptr; } FText SProjectDialog::GetCurrentProjectFilePath() const { return PathAreaSwitcher && PathAreaSwitcher->GetActiveWidgetIndex() == 1 ? FText::FromString(ProjectBrowser->GetSelectedProjectFile()) : FText::FromString(CurrentProjectFilePath); } void SProjectDialog::OnCurrentProjectFilePathChanged(const FText& InValue) { CurrentProjectFilePath = InValue.ToString(); FPaths::MakePlatformFilename(CurrentProjectFilePath); UpdateProjectFileValidity(); } void SProjectDialog::OnCurrentProjectFileNameChanged(const FText& InValue) { CurrentProjectFileName = InValue.ToString(); UpdateProjectFileValidity(); } FText SProjectDialog::GetCurrentProjectFileName() const { return PathAreaSwitcher && PathAreaSwitcher->GetActiveWidgetIndex() == 1 ? ProjectBrowser->GetSelectedProjectName() : FText::FromString(CurrentProjectFileName); } FReply SProjectDialog::HandlePathBrowseButtonClicked() { IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get(); if (DesktopPlatform) { FString FolderName; const FString Title = LOCTEXT("NewProjectBrowseTitle", "Choose a project location").ToString(); const bool bFolderSelected = DesktopPlatform->OpenDirectoryDialog( FSlateApplication::Get().FindBestParentWindowHandleForDialogs(AsShared()), Title, LastBrowsePath, FolderName ); if (bFolderSelected) { if (!FolderName.EndsWith(TEXT("/"))) { FolderName += TEXT("/"); } FPaths::MakePlatformFilename(FolderName); LastBrowsePath = FolderName; CurrentProjectFilePath = FolderName; } } return FReply::Handled(); } bool SProjectDialog::IsCompilerRequired() const { TSharedPtr SelectedTemplate = GetSelectedTemplateItem(); if (SelectedTemplate.IsValid()) { return bShouldGenerateCode && !SelectedTemplate->CodeProjectFile.IsEmpty(); } return false; } // Linux does not require an IDE to be setup to compile things bool SProjectDialog::IsIDERequired() const { #if PLATFORM_LINUX return false; #else return true; #endif } EVisibility SProjectDialog::GetProjectSettingsVisibility() const { const TArray& HiddenSettings = GetSelectedTemplateProperty(&FTemplateItem::HiddenSettings); return HiddenSettings.Contains(ETemplateSetting::All) ? EVisibility::Collapsed : EVisibility::Visible; } EVisibility SProjectDialog::GetSelectedTemplateClassVisibility() const { return GetSelectedTemplateProperty(&FTemplateItem::ClassTypes).IsEmpty() == false ? EVisibility::Visible : EVisibility::Collapsed; } FText SProjectDialog::GetSelectedTemplateAssetTypes() const { return FText::FromString(GetSelectedTemplateProperty(&FTemplateItem::AssetTypes)); } FText SProjectDialog::GetSelectedTemplateClassTypes() const { return FText::FromString(GetSelectedTemplateProperty(&FTemplateItem::ClassTypes)); } EVisibility SProjectDialog::GetSelectedTemplateAssetVisibility() const { return GetSelectedTemplateProperty(&FTemplateItem::AssetTypes).IsEmpty() == false ? EVisibility::Visible : EVisibility::Collapsed; } FString SProjectDialog::GetProjectFilenameWithPath() const { if (CurrentProjectFilePath.IsEmpty()) { // Don't even try to assemble the path or else it may be relative to the binaries folder! return TEXT(""); } else { const FString ProjectName = CurrentProjectFileName; const FString ProjectPath = IFileManager::Get().ConvertToAbsolutePathForExternalAppForWrite(*CurrentProjectFilePath); const FString Filename = ProjectName + TEXT(".") + FProjectDescriptor::GetExtension(); FString ProjectFilename = FPaths::Combine(*ProjectPath, *ProjectName, *Filename); FPaths::MakePlatformFilename(ProjectFilename); return ProjectFilename; } } void SProjectDialog::UpdateProjectFileValidity() { // Global validity { bLastGlobalValidityCheckSuccessful = true; TSharedPtr SelectedTemplate = GetSelectedTemplateItem(); if (!SelectedTemplate.IsValid()) { bLastGlobalValidityCheckSuccessful = false; LastGlobalValidityErrorText = LOCTEXT("NoTemplateSelected", "No Template Selected"); } else { if (IsCompilerRequired()) { if (!FSourceCodeNavigation::IsCompilerAvailable()) { bLastGlobalValidityCheckSuccessful = false; if (IsIDERequired()) { LastGlobalValidityErrorText = FText::Format(LOCTEXT("NoCompilerFoundProjectDialog", "No compiler was found. In order to use a C++ template, you must first install {0}."), FSourceCodeNavigation::GetSuggestedSourceCodeIDE()); } else { LastGlobalValidityErrorText = FText::Format(LOCTEXT("MissingIDEProjectDialog", "Your IDE {0} is missing or incorrectly configured, please consider using {1}"), FSourceCodeNavigation::GetSelectedSourceCodeIDE(), FSourceCodeNavigation::GetSuggestedSourceCodeIDE()); } } else if (!FDesktopPlatformModule::Get()->IsUnrealBuildToolAvailable()) { bLastGlobalValidityCheckSuccessful = false; LastGlobalValidityErrorText = LOCTEXT("UBTNotFound", "Engine source code was not found. In order to use a C++ template, you must have engine source code in Engine/Source."); } } } } // Name and Location Validity { bLastNameAndLocationValidityCheckSuccessful = true; if (!FPlatformMisc::IsValidAbsolutePathFormat(CurrentProjectFilePath)) { bLastNameAndLocationValidityCheckSuccessful = false; LastNameAndLocationValidityErrorText = LOCTEXT("InvalidFolderPath", "The folder path is invalid"); } else { FText FailReason; if (!GameProjectUtils::IsValidProjectFileForCreation(GetProjectFilenameWithPath(), FailReason)) { bLastNameAndLocationValidityCheckSuccessful = false; LastNameAndLocationValidityErrorText = FailReason; } } if (CurrentProjectFileName.Contains(TEXT("/")) || CurrentProjectFileName.Contains(TEXT("\\"))) { bLastNameAndLocationValidityCheckSuccessful = false; LastNameAndLocationValidityErrorText = LOCTEXT("SlashOrBackslashInProjectName", "The project name may not contain a slash or backslash"); } else { FText FailReason; if (!GameProjectUtils::IsValidProjectFileForCreation(GetProjectFilenameWithPath(), FailReason)) { bLastNameAndLocationValidityCheckSuccessful = false; LastNameAndLocationValidityErrorText = FailReason; } } } } void SProjectDialog::OnMajorTemplateCategorySelectionChanged(TSharedPtr Item, ESelectInfo::Type SelectType) { if(Item.IsValid()) { CurrentCategory = Item; SetCurrentMajorCategory(Item->Key); TemplateAndRecentProjectsSwitcher->SetActiveWidgetIndex(0); PathAreaSwitcher->SetActiveWidgetIndex(0); } } TSharedRef SProjectDialog::ConstructMajorCategoryTableRow(TSharedPtr Item, const TSharedRef& TableView) { TWeakPtr CurrentItem = Item; TSharedRef>> Row = SNew(STableRow>, TableView) .Style(FAppStyle::Get(), "ProjectBrowser.TableRow") .ShowSelection(false) .Padding(2.0f); TWeakPtr>> RowWeakPtr = Row; Row->SetContent( SNew(SMajorCategoryTile, Item) .IsSelected_Lambda([CurrentItem, this]() { return CurrentItem == CurrentCategory; }) ); return Row; } #undef LOCTEXT_NAMESPACE