// 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 "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 "WorldPartition/IWorldPartitionEditorModule.h" #include "Internationalization/BreakIterator.h" #include "Brushes/SlateImageBrush.h" #include "SPrimaryButton.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; bool bIsNewLevelItem; FText Name; TUniquePtr ThumbnailBrush; }; /** * 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_END_ARGS() void Construct(const FArguments& InArgs) { ParentWindowPtr = InArgs._ParentWindow.Get(); OutTemplateMapPackageName = TEXT(""); bUserClickedOkay = false; TemplateItemsList = MakeTemplateItems(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 IsTemplateChosen() const { return bUserClickedOkay; } private: static TArray> MakeTemplateItems(const TArray& TemplateMapInfos) { TArray> TemplateItems; // Build a list of items - one for each template for (const FTemplateMapInfo& TemplateMapInfo : TemplateMapInfos) { TSharedPtr Item = MakeShareable(new FNewLevelTemplateItem()); Item->TemplateMapInfo = TemplateMapInfo; Item->bIsNewLevelItem = false; 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")); } 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->bIsNewLevelItem = true; 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); 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->bIsNewLevelItem) { OutTemplateMapPackageName = Template->TemplateMapInfo.Map; } bUserClickedOkay = true; IWorldPartitionEditorModule& WorldPartitionEditorModule = FModuleManager::LoadModuleChecked("WorldPartitionEditor"); bPartitionedWorld = WorldPartitionEditorModule.IsWorldPartitionEnabled(); bExternalActors |= bPartitionedWorld; 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 bExternalActors; bool bPartitionedWorld; }; IMPLEMENT_MODULE( FNewLevelDialogModule, NewLevelDialog ); const FName FNewLevelDialogModule::NewLevelDialogAppIdentifier( TEXT( "NewLevelDialogApp" ) ); void FNewLevelDialogModule::StartupModule() { } void FNewLevelDialogModule::ShutdownModule() { } bool FNewLevelDialogModule::CreateAndShowNewLevelDialog( const TSharedPtr ParentWidget, FString& OutTemplateMapPackageName ) { TArray EmptyTemplates; return CreateAndShowTemplateDialog(ParentWidget, LOCTEXT("WindowHeader", "New Level"), GUnrealEd ? GUnrealEd->GetTemplateMapInfos() : EmptyTemplates, OutTemplateMapPackageName); } bool FNewLevelDialogModule::CreateAndShowTemplateDialog( const TSharedPtr ParentWidget, const FText& Title, const TArray& Templates, FString& OutTemplateMapPackageName ) { // 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); NewLevelWindow->SetContent(NewLevelDialog); FSlateApplication::Get().AddModalWindow(NewLevelWindow.ToSharedRef(), ParentWidget); OutTemplateMapPackageName = NewLevelDialog->GetChosenTemplate(); return NewLevelDialog->IsTemplateChosen(); } #undef LOCTEXT_NAMESPACE