// Copyright Epic Games, Inc. All Rights Reserved. #include "NewLevelDialogModule.h" #include "Layout/Margin.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/SCompoundWidget.h" #include "Widgets/SOverlay.h" #include "Widgets/SWindow.h" #include "Widgets/Text/STextBlock.h" #include "Modules/ModuleManager.h" #include "Misc/PackageName.h" #include "Misc/Paths.h" #include "Framework/Application/SlateApplication.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SScrollBorder.h" #include "Widgets/Layout/SSeparator.h" #include "Widgets/Layout/SWrapBox.h" #include "Widgets/Images/SImage.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Input/SButton.h" #include "Widgets/Views/STileView.h" #include "EditorStyleSet.h" #include "Editor/UnrealEdEngine.h" #include "Engine/Texture2D.h" #include "UnrealEdGlobals.h" #include "Internationalization/BreakIterator.h" #include "Brushes/SlateImageBrush.h" #include "SPrimaryButton.h" #include "Engine/Level.h" #define LOCTEXT_NAMESPACE "NewLevelDialog" namespace NewLevelDialogDefs { constexpr float TemplateTileHeight = 153; constexpr float TemplateTileWidth = 102; constexpr float DefaultWindowHeight = 418; constexpr float DefaultWindowWidth = 527; constexpr float LargeWindowHeight = 566; constexpr float LargeWindowWidth = 1008; constexpr float MinWindowHeight = 280; constexpr float MinWindowWidth = 320; } struct FNewLevelTemplateItem { FTemplateMapInfo TemplateMapInfo; FText Name; TUniquePtr ThumbnailBrush; enum NewLevelType { Empty, EmptyWorldPartition, Template } Type; }; /** * Single thumbnail tile for a level template in the tile view of templates. */ class SNewLevelTemplateTile : public STableRow> { public: SLATE_BEGIN_ARGS(SNewLevelTemplateTile) {} SLATE_ARGUMENT(TSharedPtr, Item) SLATE_END_ARGS() static TSharedRef BuildTile(TSharedPtr Item, const TSharedRef& OwnerTable) { if (!ensure(Item.IsValid())) { return SNew(STableRow>, OwnerTable); } return SNew(SNewLevelTemplateTile, OwnerTable).Item(Item); } void Construct(const FArguments& InArgs, const TSharedRef& OwnerTable) { check(InArgs._Item.IsValid()); STableRow::Construct( STableRow::FArguments() .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(NewLevelDialogDefs::TemplateTileWidth) .HeightOverride(NewLevelDialogDefs::TemplateTileWidth) // use width on purpose, this is a square [ SNew(SBorder) .Padding(0) .BorderImage(FAppStyle::Get().GetBrush("ProjectBrowser.ProjectTile.ThumbnailAreaBackground")) .HAlign(HAlign_Center) .VAlign(VAlign_Center) [ SNew(SImage) .Image(InArgs._Item->ThumbnailBrush.Get()) ] ] ] // Name + SVerticalBox::Slot() [ SNew(SBorder) .Padding(FMargin(5.0f, 0)) .VAlign(VAlign_Top) .Padding(FMargin(3.0f, 3.0f)) .BorderImage(FAppStyle::Get().GetBrush("ProjectBrowser.ProjectTile.NameAreaBackground")) [ SNew(STextBlock) .Font(FAppStyle::Get().GetFontStyle("ProjectBrowser.ProjectTile.Font")) .WrapTextAt(NewLevelDialogDefs::TemplateTileWidth - 4.0f) .LineBreakPolicy(FBreakIterator::CreateCamelCaseBreakIterator()) .Text(InArgs._Item->Name) .ColorAndOpacity(FAppStyle::Get().GetSlateColor("Colors.Foreground")) ] ] ] + SOverlay::Slot() [ SNew(SImage) .Visibility(EVisibility::HitTestInvisible) .Image(this, &SNewLevelTemplateTile::GetSelectionOutlineBrush) ] ] ], OwnerTable ); } private: const FSlateBrush* GetSelectionOutlineBrush() const { const bool bIsSelected = IsSelected(); 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(); } }; /** * Main widget class showing a table of level templates as labeled thumbnails * for the user to select by clicking. */ class SNewLevelDialog : public SCompoundWidget { public: SLATE_BEGIN_ARGS(SNewLevelDialog) {} /** A pointer to the parent window */ SLATE_ATTRIBUTE(TSharedPtr, ParentWindow) SLATE_ATTRIBUTE(TArray, Templates) SLATE_ATTRIBUTE(bool, bShowPartitionedTemplates) SLATE_END_ARGS() void Construct(const FArguments& InArgs) { ParentWindowPtr = InArgs._ParentWindow.Get(); OutTemplateMapPackageName = TEXT(""); bIsPartitionedWorld = false; bUserClickedOkay = false; TemplateItemsList = MakeTemplateItems(InArgs._bShowPartitionedTemplates.Get(), InArgs._Templates.Get()); TemplateListView = SNew(STileView>) .ListItemsSource(&TemplateItemsList) .SelectionMode(ESelectionMode::Single) .ClearSelectionOnClick(false) .ItemAlignment(EListItemAlignment::LeftAligned) .OnGenerateTile_Static(&SNewLevelTemplateTile::BuildTile) .OnMouseButtonDoubleClick(this, &SNewLevelDialog::HandleTemplateItemDoubleClick) .ItemHeight(NewLevelDialogDefs::TemplateTileHeight + 9) .ItemWidth(NewLevelDialogDefs::TemplateTileWidth + 9); TSharedPtr CreateButton; this->ChildSlot [ SNew(SBorder) .BorderImage(FAppStyle::Get().GetBrush("Brushes.Panel")) .Padding(FMargin(8.0f, 8.0f)) [ SNew(SVerticalBox) // Top section with template thumbnails +SVerticalBox::Slot() [ SNew(SScrollBorder, TemplateListView.ToSharedRef()) [ TemplateListView.ToSharedRef() ] ] +SVerticalBox::Slot() .AutoHeight() .Padding(-8.0f, 0.0f) [ SNew(SSeparator) .Orientation(EOrientation::Orient_Horizontal) .Thickness(2.0f) ] // Bottom section with dialog buttons +SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Right) .Padding(8.0f, 16.0f, 8.0f, 8.0f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(8.0f, 0.0f, 8.0f, 0.0f) .VAlign(VAlign_Center) .AutoWidth() [ SAssignNew(CreateButton, SPrimaryButton) .Text(LOCTEXT("Create", "Create")) .IsEnabled(this, &SNewLevelDialog::CanCreateLevel) .OnClicked(this, &SNewLevelDialog::OnCreateClicked) ] + SHorizontalBox::Slot() .Padding(8.0f, 0.0f, 0.0f, 0.0f) .VAlign(VAlign_Center) .AutoWidth() [ SNew(SButton) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .TextStyle(FAppStyle::Get(), "DialogButtonText") .Text(LOCTEXT("Cancel", "Cancel")) .OnClicked(this, &SNewLevelDialog::OnCancelClicked) ] ] ] ]; // Give the create button inital focus so that pressing enter will activate it check(CreateButton != nullptr); ParentWindowPtr.Pin().Get()->SetWidgetToFocusOnActivate( CreateButton ); // Automatically select the first template item by default if (!TemplateItemsList.IsEmpty()) { TemplateListView->SetSelection(TemplateItemsList[0]); } } FString GetChosenTemplate() const { return OutTemplateMapPackageName; } bool IsPartitionedWorld() const { return bIsPartitionedWorld; } bool IsTemplateChosen() const { return bUserClickedOkay; } private: static TArray> MakeTemplateItems(bool bShowPartitionedTemplates, const TArray& TemplateMapInfos) { TArray> TemplateItems; // Build a list of items - one for each template for (const FTemplateMapInfo& TemplateMapInfo : TemplateMapInfos) { if (!bShowPartitionedTemplates && ULevel::GetIsLevelPartitionedFromPackage(FName(*TemplateMapInfo.Map))) { continue; } TSharedPtr Item = MakeShareable(new FNewLevelTemplateItem()); Item->TemplateMapInfo = TemplateMapInfo; Item->Type = FNewLevelTemplateItem::NewLevelType::Template; Item->Name = TemplateMapInfo.DisplayName; if (const TObjectPtr& ThumbnailTexture = TemplateMapInfo.ThumbnailTexture) { // Level with thumbnail Item->ThumbnailBrush = MakeUnique( ThumbnailTexture, FVector2D(ThumbnailTexture->GetSizeX(), ThumbnailTexture->GetSizeY())); if (Item->Name.IsEmpty()) { Item->Name = FText::FromString(ThumbnailTexture->GetName().Replace(TEXT("_"), TEXT(" "))); } } else { // Level with no thumbnail Item->ThumbnailBrush = MakeUnique(*FEditorStyle::GetBrush("NewLevelDialog.Default")); if (Item->Name.IsEmpty()) { Item->Name = FText::FromString(FPaths::GetBaseFilename(TemplateMapInfo.Map)); } } check(Item->ThumbnailBrush); Item->ThumbnailBrush->OutlineSettings.CornerRadii = FVector4(4, 4, 0, 0); Item->ThumbnailBrush->OutlineSettings.RoundingType = ESlateBrushRoundingType::FixedRadius; Item->ThumbnailBrush->DrawAs = ESlateBrushDrawType::RoundedBox; TemplateItems.Add(Item); } // Add an extra item for creating a new, blank level TSharedPtr NewItem = MakeShareable(new FNewLevelTemplateItem()); NewItem->Type = FNewLevelTemplateItem::NewLevelType::Empty; NewItem->Name = LOCTEXT("NewLevelItemLabel", "Empty Level"); NewItem->ThumbnailBrush = MakeUnique(*FEditorStyle::GetBrush("NewLevelDialog.Blank")); NewItem->ThumbnailBrush->OutlineSettings.CornerRadii = FVector4(4, 4, 0, 0); NewItem->ThumbnailBrush->OutlineSettings.RoundingType = ESlateBrushRoundingType::FixedRadius; NewItem->ThumbnailBrush->DrawAs = ESlateBrushDrawType::RoundedBox; TemplateItems.Add(NewItem); if (bShowPartitionedTemplates) { // Add an extra item for creating a new, blank level TSharedPtr NewItemWP = MakeShareable(new FNewLevelTemplateItem()); NewItemWP->Type = FNewLevelTemplateItem::NewLevelType::EmptyWorldPartition; NewItemWP->Name = LOCTEXT("NewWPLevelItemLabel", "Empty Open World"); NewItemWP->ThumbnailBrush = MakeUnique(*FEditorStyle::GetBrush("NewLevelDialog.BlankWP")); NewItemWP->ThumbnailBrush->OutlineSettings.CornerRadii = FVector4(4, 4, 0, 0); NewItemWP->ThumbnailBrush->OutlineSettings.RoundingType = ESlateBrushRoundingType::FixedRadius; NewItemWP->ThumbnailBrush->DrawAs = ESlateBrushDrawType::RoundedBox; TemplateItems.Add(NewItemWP); } return TemplateItems; } bool CanCreateLevel() const { if (!ensure(TemplateListView.IsValid())) { return false; } return TemplateListView->GetNumItemsSelected() == 1; } FReply OnCreateClicked() { if (!ensure(TemplateListView.IsValid())) { return FReply::Handled(); } const TArray> Items = TemplateListView->GetSelectedItems(); if (!ensure(Items.Num() == 1)) { return FReply::Handled(); } const TSharedPtr Template = Items[0]; if (Template->Type == FNewLevelTemplateItem::NewLevelType::Template) { OutTemplateMapPackageName = Template->TemplateMapInfo.Map; } else if (Template->Type == FNewLevelTemplateItem::NewLevelType::EmptyWorldPartition) { bIsPartitionedWorld = true; } bUserClickedOkay = true; ParentWindowPtr.Pin()->RequestDestroyWindow(); return FReply::Handled(); } FReply OnCancelClicked() { bUserClickedOkay = false; ParentWindowPtr.Pin()->RequestDestroyWindow(); return FReply::Handled(); } virtual FReply OnKeyDown( const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent ) override { if( InKeyEvent.GetKey() == EKeys::Escape ) { return OnCancelClicked(); } return SCompoundWidget::OnKeyDown( MyGeometry, InKeyEvent ); } void HandleTemplateItemDoubleClick(TSharedPtr) { OnCreateClicked(); } /** Pointer to the parent window, so we know to destroy it when done */ TWeakPtr ParentWindowPtr; /** Initial size of the parent window */ FVector2D InitialWindowSize; TArray> TemplateItemsList; TSharedPtr < STileView> > TemplateListView; FString OutTemplateMapPackageName; bool bUserClickedOkay; bool bIsPartitionedWorld; }; IMPLEMENT_MODULE( FNewLevelDialogModule, NewLevelDialog ); const FName FNewLevelDialogModule::NewLevelDialogAppIdentifier( TEXT( "NewLevelDialogApp" ) ); void FNewLevelDialogModule::StartupModule() { } void FNewLevelDialogModule::ShutdownModule() { } bool FNewLevelDialogModule::CreateAndShowNewLevelDialog( const TSharedPtr ParentWidget, FString& OutTemplateMapPackageName, bool bShowPartitionedTemplates, bool& bOutIsPartitionedWorld) { TArray EmptyTemplates; return CreateAndShowTemplateDialog(ParentWidget, LOCTEXT("WindowHeader", "New Level"), GUnrealEd ? GUnrealEd->GetTemplateMapInfos() : EmptyTemplates, OutTemplateMapPackageName, bShowPartitionedTemplates, bOutIsPartitionedWorld); } bool FNewLevelDialogModule::CreateAndShowTemplateDialog( const TSharedPtr ParentWidget, const FText& Title, const TArray& Templates, FString& OutTemplateMapPackageName, bool bShowPartitionedTemplates, bool& bOutIsPartitionedWorld) { // Open larger window if there are enough templates FVector2D WindowClientSize(NewLevelDialogDefs::DefaultWindowWidth, NewLevelDialogDefs::DefaultWindowHeight); if (Templates.Num() > 9) { WindowClientSize = FVector2D(NewLevelDialogDefs::LargeWindowWidth, NewLevelDialogDefs::LargeWindowHeight); } TSharedPtr NewLevelWindow = SNew(SWindow) .Title(Title) .ClientSize(WindowClientSize) .MinHeight(NewLevelDialogDefs::MinWindowHeight) .MinWidth(NewLevelDialogDefs::MinWindowWidth) .SizingRule( ESizingRule::UserSized ) .SupportsMinimize(false) .SupportsMaximize(false); TSharedRef NewLevelDialog = SNew(SNewLevelDialog) .ParentWindow(NewLevelWindow) .Templates(Templates) .bShowPartitionedTemplates(bShowPartitionedTemplates); NewLevelWindow->SetContent(NewLevelDialog); FSlateApplication::Get().AddModalWindow(NewLevelWindow.ToSharedRef(), ParentWidget); OutTemplateMapPackageName = NewLevelDialog->GetChosenTemplate(); bOutIsPartitionedWorld = NewLevelDialog->IsPartitionedWorld(); return NewLevelDialog->IsTemplateChosen(); } #undef LOCTEXT_NAMESPACE