Files
UnrealEngine/Engine/Source/Editor/NewLevelDialog/Private/NewLevelDialogModule.cpp
2025-05-18 13:04:45 +08:00

617 lines
19 KiB
C++

// 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 "Styling/AppStyle.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"
#include "Styling/StyleColors.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;
FString Category;
TUniquePtr<FSlateBrush> ThumbnailBrush;
TObjectPtr<UTexture2D> ThumbnailAsset = nullptr;
int32 OriginalIndex = INDEX_NONE;
enum NewLevelType
{
Empty,
EmptyWorldPartition,
Template
} Type;
};
/**
* Single thumbnail tile for a level template in the tile view of templates.
*/
class SNewLevelTemplateTile : public STableRow<TSharedPtr<FNewLevelTemplateItem>>
{
public:
SLATE_BEGIN_ARGS(SNewLevelTemplateTile) {}
SLATE_ARGUMENT(TSharedPtr<FNewLevelTemplateItem>, Item)
SLATE_END_ARGS()
static TSharedRef<ITableRow> BuildTile(TSharedPtr<FNewLevelTemplateItem> Item, const TSharedRef<STableViewBase>& OwnerTable)
{
if (!ensure(Item.IsValid()))
{
return SNew(STableRow<TSharedPtr<FNewLevelTemplateItem>>, OwnerTable);
}
return SNew(SNewLevelTemplateTile, OwnerTable).Item(Item);
}
void Construct(const FArguments& InArgs, const TSharedRef<STableViewBase>& 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(FMargin(0))
.BorderImage(FAppStyle::Get().GetBrush("ProjectBrowser.ProjectTile.ThumbnailAreaBackground"))
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
[
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(this, &SNewLevelTemplateTile::GetNameAreaBackgroundBrush)
[
SNew(STextBlock)
.Font(FAppStyle::Get().GetFontStyle("ProjectBrowser.ProjectTile.Font"))
.WrapTextAt(NewLevelDialogDefs::TemplateTileWidth - 4.0f)
.LineBreakPolicy(FBreakIterator::CreateCamelCaseBreakIterator())
.Text(InArgs._Item->Name)
.ColorAndOpacity(this, &SNewLevelTemplateTile::GetNameAreaTextColor)
]
]
]
+ 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();
}
const FSlateBrush* GetNameAreaBackgroundBrush() const
{
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");
}
FSlateColor GetNameAreaTextColor() const
{
const bool bIsSelected = IsSelected();
const bool bIsRowHovered = IsHovered();
if (bIsSelected || bIsRowHovered)
{
return FStyleColors::White;
}
return FSlateColor::UseForeground();
}
};
/**
* Main widget class showing a table of level templates as labeled thumbnails
* for the user to select by clicking.
*/
class SNewLevelDialog : public SCompoundWidget, public FGCObject
{
public:
SLATE_BEGIN_ARGS(SNewLevelDialog) {}
/** A pointer to the parent window */
SLATE_ATTRIBUTE(TSharedPtr<SWindow>, ParentWindow)
SLATE_ATTRIBUTE(TArray<FTemplateMapInfo>, Templates)
SLATE_ATTRIBUTE(bool, bShowPartitionedTemplates)
SLATE_ATTRIBUTE(FNewLevelDialogModule::EShowEmptyTemplate, ShowEmptyTemplate)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs)
{
ParentWindowPtr = InArgs._ParentWindow.Get();
TemplateItemsList = MakeTemplateItems(InArgs._bShowPartitionedTemplates.Get(), InArgs._ShowEmptyTemplate.Get(), InArgs._Templates.Get());
TemplateListView = SNew(STileView<TSharedPtr<FNewLevelTemplateItem>>)
.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<SButton> 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
{
FString Result;
if (OutTemplateItem.IsValid())
{
Result = OutTemplateItem->TemplateMapInfo.Map.GetLongPackageName();
// For backwards compatibility, handle the case where the map name format does not include the object name directly
if (Result.IsEmpty())
{
Result = OutTemplateItem->TemplateMapInfo.Map.GetAssetPathString();
}
}
return Result;
}
FTemplateMapInfo GetChosenTemplateMapInfo() const { return OutTemplateItem.IsValid() ? OutTemplateItem->TemplateMapInfo : FTemplateMapInfo(); }
bool IsPartitionedWorld() const { return OutTemplateItem.IsValid() && OutTemplateItem->Type == FNewLevelTemplateItem::NewLevelType::EmptyWorldPartition; }
bool IsTemplateChosen() const { return OutTemplateItem.IsValid(); }
//~ Begin FGCObject Interface.
virtual void AddReferencedObjects(FReferenceCollector& Collector) override
{
for (const TSharedPtr<FNewLevelTemplateItem>& It : TemplateItemsList)
{
if (It.IsValid() && It->ThumbnailAsset)
{
Collector.AddReferencedObject(It->ThumbnailAsset);
}
}
}
virtual FString GetReferencerName() const override
{
return TEXT("SNewLevelDialog");
}
//~ End FGCObject Interface.
private:
static TArray<TSharedPtr<FNewLevelTemplateItem>> MakeTemplateItems(bool bShowPartitionedTemplates, FNewLevelDialogModule::EShowEmptyTemplate ShowEmptyTemplate, const TArray<FTemplateMapInfo>& TemplateMapInfos)
{
TArray<TSharedPtr<FNewLevelTemplateItem>> TemplateItems;
// Build a list of items - one for each template
int32 CurrentIndex = 0;
for (const FTemplateMapInfo& TemplateMapInfo : TemplateMapInfos)
{
if (!bShowPartitionedTemplates && ULevel::GetIsLevelPartitionedFromPackage(FName(*TemplateMapInfo.Map.ToString())))
{
CurrentIndex++;
continue;
}
TSharedPtr<FNewLevelTemplateItem> Item = MakeShareable(new FNewLevelTemplateItem());
Item->TemplateMapInfo = TemplateMapInfo;
Item->Type = FNewLevelTemplateItem::NewLevelType::Template;
Item->Name = TemplateMapInfo.DisplayName;
Item->Category = TemplateMapInfo.Category;
Item->OriginalIndex = CurrentIndex++;
TSoftObjectPtr<UTexture2D> ThumbnailSoftObjectPtr;
if (TemplateMapInfo.Thumbnail.IsValid())
{
ThumbnailSoftObjectPtr = TemplateMapInfo.Thumbnail;
}
else if (!TemplateMapInfo.ThumbnailTexture.IsNull())
{
ThumbnailSoftObjectPtr = TemplateMapInfo.ThumbnailTexture;
}
UTexture2D* ThumbnailTexture = nullptr;
if (!ThumbnailSoftObjectPtr.IsNull())
{
ThumbnailTexture = ThumbnailSoftObjectPtr.Get();
if (!ThumbnailTexture)
{
ThumbnailTexture = ThumbnailSoftObjectPtr.LoadSynchronous();
if (ThumbnailTexture)
{
// Avoid calling UpdateResource on cooked texture as doing so will destroy the texture's data
if (!ThumbnailTexture->GetPackage()->HasAllPackagesFlags(PKG_FilterEditorOnly))
{
// Newly loaded texture requires async work to complete before being rendered in modal dialog
ThumbnailTexture->FinishCachePlatformData();
ThumbnailTexture->UpdateResource();
}
}
}
}
if (ThumbnailTexture)
{
// Reference to prevent garbage collection
Item->ThumbnailAsset = ThumbnailTexture;
// Level with thumbnail
Item->ThumbnailBrush = MakeUnique<FSlateImageBrush>(
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<FSlateBrush>(*FAppStyle::Get().GetBrush("UnrealDefaultThumbnail"));
if (Item->Name.IsEmpty())
{
Item->Name = FText::FromString(FPaths::GetBaseFilename(TemplateMapInfo.Map.ToString()));
}
}
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
if (ShowEmptyTemplate == FNewLevelDialogModule::EShowEmptyTemplate::Show)
{
TSharedPtr<FNewLevelTemplateItem> NewItem = MakeShareable(new FNewLevelTemplateItem());
NewItem->Type = FNewLevelTemplateItem::NewLevelType::Empty;
NewItem->Name = LOCTEXT("NewLevelItemLabel", "Empty Level");
NewItem->ThumbnailBrush = MakeUnique<FSlateBrush>(*FAppStyle::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);
}
const FString& OpenWorldCategory = TEXT("OpenWorld");
if (bShowPartitionedTemplates)
{
// Add an extra item for creating a new, blank level
TSharedPtr<FNewLevelTemplateItem> NewItemWP = MakeShareable(new FNewLevelTemplateItem());
NewItemWP->Type = FNewLevelTemplateItem::NewLevelType::EmptyWorldPartition;
NewItemWP->Name = LOCTEXT("NewWPLevelItemLabel", "Empty Open World");
NewItemWP->Category = OpenWorldCategory;
NewItemWP->ThumbnailBrush = MakeUnique<FSlateBrush>(*FAppStyle::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);
}
// OpenWorld category always first, If Categories are the same compares Type which will put Templates before Empty maps
TemplateItems.Sort([&OpenWorldCategory](const TSharedPtr<FNewLevelTemplateItem>& LHS, const TSharedPtr<FNewLevelTemplateItem>& RHS)
{
if (LHS->Category == RHS->Category)
{
if (LHS->Type == RHS->Type)
{
return LHS->OriginalIndex < RHS->OriginalIndex;
}
return LHS->Type > RHS->Type;
}
else if (LHS->Category == OpenWorldCategory)
{
return true;
}
else if (RHS->Category == OpenWorldCategory)
{
return false;
}
return LHS->Category > RHS->Category;
});
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<TSharedPtr<FNewLevelTemplateItem>> Items = TemplateListView->GetSelectedItems();
if (!ensure(Items.Num() == 1))
{
return FReply::Handled();
}
OutTemplateItem = Items[0];
ParentWindowPtr.Pin()->RequestDestroyWindow();
return FReply::Handled();
}
FReply OnCancelClicked()
{
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<FNewLevelTemplateItem>)
{
OnCreateClicked();
}
/** Pointer to the parent window, so we know to destroy it when done */
TWeakPtr<SWindow> ParentWindowPtr;
/** Initial size of the parent window */
FVector2D InitialWindowSize;
TArray<TSharedPtr<FNewLevelTemplateItem>> TemplateItemsList;
TSharedPtr < STileView<TSharedPtr<FNewLevelTemplateItem>> > TemplateListView;
TSharedPtr<FNewLevelTemplateItem> OutTemplateItem;
};
IMPLEMENT_MODULE( FNewLevelDialogModule, NewLevelDialog );
const FName FNewLevelDialogModule::NewLevelDialogAppIdentifier( TEXT( "NewLevelDialogApp" ) );
void FNewLevelDialogModule::StartupModule()
{
}
void FNewLevelDialogModule::ShutdownModule()
{
}
bool FNewLevelDialogModule::CreateAndShowNewLevelDialog( const TSharedPtr<const SWidget> ParentWidget, FString& OutTemplateMapPackageName, bool bShowPartitionedTemplates, bool& bOutIsPartitionedWorld, FTemplateMapInfo* OutTemplateMapInfo, EShowEmptyTemplate ShowEmptyTemplate)
{
TArray<FTemplateMapInfo> EmptyTemplates;
return CreateAndShowTemplateDialog(ParentWidget, LOCTEXT("WindowHeader", "New Level"), GUnrealEd ? GUnrealEd->GetTemplateMapInfos() : EmptyTemplates, OutTemplateMapPackageName, bShowPartitionedTemplates, bOutIsPartitionedWorld, OutTemplateMapInfo, ShowEmptyTemplate);
}
bool FNewLevelDialogModule::CreateAndShowTemplateDialog( const TSharedPtr<const SWidget> ParentWidget, const FText& Title, const TArray<FTemplateMapInfo>& Templates, FString& OutTemplateMapPackageName, bool bShowPartitionedTemplates, bool& bOutIsPartitionedWorld, FTemplateMapInfo* OutTemplateMapInfo, EShowEmptyTemplate ShowEmptyTemplate)
{
// 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<SWindow> NewLevelWindow =
SNew(SWindow)
.Title(Title)
.ClientSize(WindowClientSize)
.MinHeight(NewLevelDialogDefs::MinWindowHeight)
.MinWidth(NewLevelDialogDefs::MinWindowWidth)
.SizingRule( ESizingRule::UserSized )
.SupportsMinimize(false)
.SupportsMaximize(false);
TSharedRef<SNewLevelDialog> NewLevelDialog =
SNew(SNewLevelDialog)
.ParentWindow(NewLevelWindow)
.Templates(Templates)
.bShowPartitionedTemplates(bShowPartitionedTemplates)
.ShowEmptyTemplate(ShowEmptyTemplate);
NewLevelWindow->SetContent(NewLevelDialog);
FSlateApplication::Get().AddModalWindow(NewLevelWindow.ToSharedRef(), ParentWidget);
if (OutTemplateMapInfo)
{
*OutTemplateMapInfo = NewLevelDialog->GetChosenTemplateMapInfo();
}
OutTemplateMapPackageName = NewLevelDialog->GetChosenTemplate();
bOutIsPartitionedWorld = NewLevelDialog->IsPartitionedWorld();
return NewLevelDialog->IsTemplateChosen();
}
#undef LOCTEXT_NAMESPACE