BaseLevel selection popup with thumbnail grid
- Add Thumbnail field to FPS_Editor_BaseLevelEntry (UTexture2D) - Replace BaseLevel combo box with button that opens a selection popup - Popup displays base levels in a 3-column grid with thumbnails (2:1 ratio) - Validate/Cancel buttons to confirm or dismiss selection - Gray placeholder shown for entries without thumbnail Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -54,7 +54,7 @@ struct FPS_Editor_BaseLevelEntry
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Display name shown in the editor combo box and used for scene filtering. */
|
||||
/** Display name shown in the editor UI and used for scene filtering. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||
FString DisplayName;
|
||||
|
||||
@@ -64,6 +64,10 @@ struct FPS_Editor_BaseLevelEntry
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||
FString MapPath;
|
||||
|
||||
/** Thumbnail image shown in the BaseLevel selection popup. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||
TObjectPtr<UTexture2D> Thumbnail;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -266,6 +266,69 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
||||
]
|
||||
]
|
||||
]
|
||||
// BaseLevel selection popup
|
||||
+ SOverlay::Slot()
|
||||
.HAlign(HAlign_Center)
|
||||
.VAlign(VAlign_Center)
|
||||
[
|
||||
SAssignNew(BaseLevelPopup, SBorder)
|
||||
.Visibility(EVisibility::Collapsed)
|
||||
.BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush"))
|
||||
.BorderBackgroundColor(FLinearColor(0.08f, 0.08f, 0.08f, 0.97f))
|
||||
.Padding(FMargin(24.0f, 16.0f))
|
||||
[
|
||||
SNew(SVerticalBox)
|
||||
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 0.0f, 0.0f, 12.0f)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("Select Base Level")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 14))
|
||||
.ColorAndOpacity(FLinearColor::White)
|
||||
]
|
||||
+ SVerticalBox::Slot().FillHeight(1.0f).MaxHeight(400.0f)
|
||||
[
|
||||
SNew(SScrollBox)
|
||||
.ConsumeMouseWheel(EConsumeMouseWheel::Always)
|
||||
+ SScrollBox::Slot()
|
||||
[
|
||||
SAssignNew(BaseLevelGridContainer, SVerticalBox)
|
||||
]
|
||||
]
|
||||
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 12.0f, 0.0f, 0.0f)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
.OnClicked_Lambda([this]() -> FReply
|
||||
{
|
||||
ConfirmBaseLevelSelection();
|
||||
return FReply::Handled();
|
||||
})
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("Validate")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 11))
|
||||
.ColorAndOpacity(FLinearColor(0.2f, 0.9f, 0.2f))
|
||||
]
|
||||
]
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
.OnClicked_Lambda([this]() -> FReply
|
||||
{
|
||||
CancelBaseLevelPopup();
|
||||
return FReply::Handled();
|
||||
})
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("Cancel")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 11))
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
// Close confirmation overlay
|
||||
+ SOverlay::Slot()
|
||||
.HAlign(HAlign_Center)
|
||||
@@ -868,8 +931,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
||||
{
|
||||
if (*Opt == SceneLevelName)
|
||||
{
|
||||
if (BaseLevelCombo.IsValid()) BaseLevelCombo->SetSelectedItem(Opt);
|
||||
if (BaseLevelComboText.IsValid()) BaseLevelComboText->SetText(FText::FromString(SceneLevelName));
|
||||
if (BaseLevelButtonText.IsValid()) BaseLevelButtonText->SetText(FText::FromString(SceneLevelName));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -896,7 +958,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
||||
]
|
||||
]
|
||||
]
|
||||
// BaseLevel combo box
|
||||
// BaseLevel selection button
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0.0f, 4.0f, 0.0f, 2.0f)
|
||||
@@ -911,54 +973,14 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
||||
]
|
||||
+ SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center)
|
||||
[
|
||||
SAssignNew(BaseLevelCombo, SComboBox<TSharedPtr<FString>>)
|
||||
.OptionsSource(&BaseLevelOptions)
|
||||
.Method(EPopupMethod::UseCurrentWindow)
|
||||
.OnSelectionChanged_Lambda([this](TSharedPtr<FString> Selected, ESelectInfo::Type SelectInfo)
|
||||
SNew(SButton)
|
||||
.OnClicked_Lambda([this]() -> FReply
|
||||
{
|
||||
if (!Selected.IsValid()) return;
|
||||
// Ignore programmatic selection (e.g. from NativeConstruct init)
|
||||
if (SelectInfo == ESelectInfo::Direct) return;
|
||||
|
||||
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer());
|
||||
if (!EditorPC) return;
|
||||
|
||||
// Clear current scenario before switching BaseLevel
|
||||
if (UPS_Editor_SceneSerializer* Ser = EditorPC->GetSceneSerializer())
|
||||
{
|
||||
if (UPS_Editor_SpawnManager* SM = EditorPC->GetSpawnManager())
|
||||
{
|
||||
Ser->ClearScene(SM);
|
||||
}
|
||||
}
|
||||
if (SaveNameField.IsValid())
|
||||
{
|
||||
SaveNameField->SetText(FText::GetEmpty());
|
||||
}
|
||||
|
||||
FString SublevelPath = *Selected;
|
||||
if (FString* Found = BaseLevelPathMap.Find(*Selected))
|
||||
{
|
||||
SublevelPath = *Found;
|
||||
}
|
||||
EditorPC->LoadBaseLevelAsSublevel(*Selected, SublevelPath);
|
||||
CurrentLevelFilter = EditorPC->CurrentBaseLevel;
|
||||
|
||||
if (BaseLevelComboText.IsValid())
|
||||
{
|
||||
BaseLevelComboText->SetText(FText::FromString(*Selected));
|
||||
}
|
||||
PopulateSceneList();
|
||||
LastOutlinerActorCount = -1; // Force outliner refresh
|
||||
})
|
||||
.OnGenerateWidget_Lambda([](TSharedPtr<FString> Item) -> TSharedRef<SWidget>
|
||||
{
|
||||
return SNew(STextBlock)
|
||||
.Text(FText::FromString(Item.IsValid() ? *Item : TEXT("")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9));
|
||||
OpenBaseLevelPopup();
|
||||
return FReply::Handled();
|
||||
})
|
||||
[
|
||||
SAssignNew(BaseLevelComboText, STextBlock)
|
||||
SAssignNew(BaseLevelButtonText, STextBlock)
|
||||
.Text(FText::FromString(TEXT("Select...")))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 9))
|
||||
.ColorAndOpacity(FLinearColor(0.4f, 0.8f, 0.4f))
|
||||
@@ -1022,7 +1044,7 @@ void UPS_Editor_MainWidget::NativeConstruct()
|
||||
Super::NativeConstruct();
|
||||
PopulateSpawnCatalog();
|
||||
|
||||
// Populate BaseLevel combo from MasterCatalog
|
||||
// Populate BaseLevel options from MasterCatalog
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||
{
|
||||
BaseLevelOptions.Add(MakeShared<FString>(TEXT("None")));
|
||||
@@ -1035,22 +1057,18 @@ void UPS_Editor_MainWidget::NativeConstruct()
|
||||
{
|
||||
BaseLevelPathMap.Add(Entry.DisplayName, Entry.MapPath);
|
||||
}
|
||||
if (Entry.Thumbnail)
|
||||
{
|
||||
BaseLevelThumbnails.Add(Entry.DisplayName, Entry.Thumbnail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If a BaseLevel was set (e.g. from subsystem), select it in the combo
|
||||
// If a BaseLevel was set (e.g. from subsystem), update the button text
|
||||
CurrentLevelFilter = EditorPC->CurrentBaseLevel;
|
||||
if (!CurrentLevelFilter.IsEmpty())
|
||||
if (!CurrentLevelFilter.IsEmpty() && BaseLevelButtonText.IsValid())
|
||||
{
|
||||
for (const TSharedPtr<FString>& Opt : BaseLevelOptions)
|
||||
{
|
||||
if (*Opt == CurrentLevelFilter)
|
||||
{
|
||||
if (BaseLevelCombo.IsValid()) BaseLevelCombo->SetSelectedItem(Opt);
|
||||
if (BaseLevelComboText.IsValid()) BaseLevelComboText->SetText(FText::FromString(CurrentLevelFilter));
|
||||
break;
|
||||
}
|
||||
}
|
||||
BaseLevelButtonText->SetText(FText::FromString(CurrentLevelFilter));
|
||||
}
|
||||
|
||||
// Restore scenario name in save field (from subsystem LastEdited)
|
||||
@@ -1134,14 +1152,9 @@ void UPS_Editor_MainWidget::PopulateSceneList()
|
||||
EditorPC->LoadBaseLevelAsSublevel(SceneLevelName, SceneMapPath);
|
||||
CurrentLevelFilter = EditorPC->CurrentBaseLevel;
|
||||
|
||||
for (const TSharedPtr<FString>& Opt : BaseLevelOptions)
|
||||
if (BaseLevelButtonText.IsValid())
|
||||
{
|
||||
if (*Opt == SceneLevelName)
|
||||
{
|
||||
if (BaseLevelCombo.IsValid()) BaseLevelCombo->SetSelectedItem(Opt);
|
||||
if (BaseLevelComboText.IsValid()) BaseLevelComboText->SetText(FText::FromString(SceneLevelName));
|
||||
break;
|
||||
}
|
||||
BaseLevelButtonText->SetText(FText::FromString(SceneLevelName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2305,6 +2318,177 @@ void UPS_Editor_MainWidget::ShowLoadingScreen(bool bShow)
|
||||
}
|
||||
}
|
||||
|
||||
void UPS_Editor_MainWidget::OpenBaseLevelPopup()
|
||||
{
|
||||
if (!BaseLevelGridContainer.IsValid() || !BaseLevelPopup.IsValid()) return;
|
||||
|
||||
BaseLevelGridContainer->ClearChildren();
|
||||
PendingBaseLevelSelection.Empty();
|
||||
|
||||
const float CardWidth = 160.0f;
|
||||
const float CardHeight = 80.0f; // 2:1 ratio
|
||||
const int32 Columns = 3;
|
||||
TSharedPtr<SHorizontalBox> CurrentRow;
|
||||
int32 ColIndex = 0;
|
||||
|
||||
for (const TSharedPtr<FString>& Option : BaseLevelOptions)
|
||||
{
|
||||
if (!Option.IsValid()) continue;
|
||||
const FString Name = *Option;
|
||||
|
||||
// Start new row if needed
|
||||
if (ColIndex % Columns == 0)
|
||||
{
|
||||
CurrentRow = SNew(SHorizontalBox);
|
||||
BaseLevelGridContainer->AddSlot().AutoHeight().Padding(0.0f, 4.0f)
|
||||
[
|
||||
CurrentRow.ToSharedRef()
|
||||
];
|
||||
}
|
||||
|
||||
TSharedRef<SVerticalBox> Card = SNew(SVerticalBox);
|
||||
|
||||
// Thumbnail
|
||||
TObjectPtr<UTexture2D>* FoundThumb = BaseLevelThumbnails.Find(Name);
|
||||
if (FoundThumb && *FoundThumb)
|
||||
{
|
||||
TSharedPtr<FSlateBrush> Brush = MakeShared<FSlateBrush>();
|
||||
Brush->SetResourceObject(*FoundThumb);
|
||||
Brush->ImageSize = FVector2D(CardWidth, CardHeight);
|
||||
Brush->DrawAs = ESlateBrushDrawType::Image;
|
||||
Brush->Tiling = ESlateBrushTileType::NoTile;
|
||||
CachedBrushes.Add(Brush);
|
||||
|
||||
TWeakPtr<FSlateBrush> WeakBrush = Brush;
|
||||
Card->AddSlot().AutoHeight().HAlign(HAlign_Center)
|
||||
[
|
||||
SNew(SImage)
|
||||
.Image_Lambda([WeakBrush]() -> const FSlateBrush*
|
||||
{
|
||||
return WeakBrush.IsValid() ? WeakBrush.Pin().Get() : nullptr;
|
||||
})
|
||||
.DesiredSizeOverride(FVector2D(CardWidth, CardHeight))
|
||||
];
|
||||
}
|
||||
else
|
||||
{
|
||||
Card->AddSlot().AutoHeight().HAlign(HAlign_Center)
|
||||
[
|
||||
SNew(SBorder)
|
||||
.BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush"))
|
||||
.BorderBackgroundColor(FLinearColor(0.15f, 0.15f, 0.15f))
|
||||
[
|
||||
SNew(SBox)
|
||||
.WidthOverride(CardWidth)
|
||||
.HeightOverride(CardHeight)
|
||||
.HAlign(HAlign_Center)
|
||||
.VAlign(VAlign_Center)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(Name))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Italic", 9))
|
||||
.ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f))
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// Name label
|
||||
Card->AddSlot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 4.0f, 0.0f, 0.0f)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(Name))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||
.ColorAndOpacity(FLinearColor::White)
|
||||
];
|
||||
|
||||
// Add card to current row
|
||||
CurrentRow->AddSlot().AutoWidth().Padding(8.0f, 0.0f)
|
||||
[
|
||||
SNew(SBox)
|
||||
.MinDesiredWidth(CardWidth + 16.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
.OnClicked_Lambda([this, Name]() -> FReply
|
||||
{
|
||||
PendingBaseLevelSelection = Name;
|
||||
return FReply::Handled();
|
||||
})
|
||||
[
|
||||
Card
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
ColIndex++;
|
||||
}
|
||||
|
||||
BaseLevelPopup->SetVisibility(EVisibility::Visible);
|
||||
}
|
||||
|
||||
void UPS_Editor_MainWidget::ConfirmBaseLevelSelection()
|
||||
{
|
||||
if (BaseLevelPopup.IsValid())
|
||||
{
|
||||
BaseLevelPopup->SetVisibility(EVisibility::Collapsed);
|
||||
}
|
||||
|
||||
if (PendingBaseLevelSelection.IsEmpty()) return;
|
||||
|
||||
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer());
|
||||
if (!EditorPC) return;
|
||||
|
||||
// Clear current scenario before switching BaseLevel
|
||||
if (UPS_Editor_SceneSerializer* Ser = EditorPC->GetSceneSerializer())
|
||||
{
|
||||
if (UPS_Editor_SpawnManager* SM = EditorPC->GetSpawnManager())
|
||||
{
|
||||
Ser->ClearScene(SM);
|
||||
}
|
||||
}
|
||||
if (SaveNameField.IsValid())
|
||||
{
|
||||
SaveNameField->SetText(FText::GetEmpty());
|
||||
}
|
||||
|
||||
// Load the selected BaseLevel
|
||||
FString SublevelPath = PendingBaseLevelSelection;
|
||||
if (FString* Found = BaseLevelPathMap.Find(PendingBaseLevelSelection))
|
||||
{
|
||||
SublevelPath = *Found;
|
||||
}
|
||||
|
||||
if (PendingBaseLevelSelection == TEXT("None"))
|
||||
{
|
||||
EditorPC->LoadBaseLevelAsSublevel(TEXT("None"));
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorPC->LoadBaseLevelAsSublevel(PendingBaseLevelSelection, SublevelPath);
|
||||
}
|
||||
|
||||
CurrentLevelFilter = EditorPC->CurrentBaseLevel;
|
||||
|
||||
if (BaseLevelButtonText.IsValid())
|
||||
{
|
||||
BaseLevelButtonText->SetText(FText::FromString(
|
||||
PendingBaseLevelSelection == TEXT("None") ? TEXT("Select...") : PendingBaseLevelSelection));
|
||||
}
|
||||
|
||||
PopulateSceneList();
|
||||
LastOutlinerActorCount = -1;
|
||||
PendingBaseLevelSelection.Empty();
|
||||
}
|
||||
|
||||
void UPS_Editor_MainWidget::CancelBaseLevelPopup()
|
||||
{
|
||||
if (BaseLevelPopup.IsValid())
|
||||
{
|
||||
BaseLevelPopup->SetVisibility(EVisibility::Collapsed);
|
||||
}
|
||||
PendingBaseLevelSelection.Empty();
|
||||
}
|
||||
|
||||
void UPS_Editor_MainWidget::OnCloseClicked()
|
||||
{
|
||||
if (CloseConfirmOverlay.IsValid())
|
||||
|
||||
@@ -77,14 +77,23 @@ private:
|
||||
// Scene save/load
|
||||
TSharedPtr<SEditableTextBox> SaveNameField;
|
||||
TSharedPtr<SVerticalBox> SceneListContainer;
|
||||
TSharedPtr<SComboBox<TSharedPtr<FString>>> BaseLevelCombo;
|
||||
TArray<TSharedPtr<FString>> BaseLevelOptions;
|
||||
TSharedPtr<STextBlock> BaseLevelComboText;
|
||||
TSharedPtr<STextBlock> BaseLevelButtonText;
|
||||
FString CurrentLevelFilter;
|
||||
/** Maps DisplayName → full MapPath for base levels (from MasterCatalog). */
|
||||
TMap<FString, FString> BaseLevelPathMap;
|
||||
/** Maps DisplayName → Thumbnail texture for base levels. */
|
||||
TMap<FString, TObjectPtr<UTexture2D>> BaseLevelThumbnails;
|
||||
TArray<TSharedPtr<FSlateBrush>> CachedBrushes;
|
||||
|
||||
// BaseLevel selection popup
|
||||
TSharedPtr<SWidget> BaseLevelPopup;
|
||||
TSharedPtr<SVerticalBox> BaseLevelGridContainer;
|
||||
FString PendingBaseLevelSelection;
|
||||
void OpenBaseLevelPopup();
|
||||
void ConfirmBaseLevelSelection();
|
||||
void CancelBaseLevelPopup();
|
||||
|
||||
// Transform fields
|
||||
TSharedPtr<SEditableTextBox> LocX, LocY, LocZ;
|
||||
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
|
||||
|
||||
Reference in New Issue
Block a user