From 5859d3bf6a62801ffc34466e9c269693b6db5382 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Wed, 15 Apr 2026 08:42:03 +0200 Subject: [PATCH] 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) --- .../PS_Editor/Spawn/PS_Editor_SpawnCatalog.h | 6 +- .../UI/Widgets/PS_Editor_MainWidget.cpp | 318 ++++++++++++++---- .../UI/Widgets/PS_Editor_MainWidget.h | 13 +- 3 files changed, 267 insertions(+), 70 deletions(-) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalog.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalog.h index ebd38ba..493d2cf 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalog.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnCatalog.h @@ -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 Thumbnail; }; /** diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp index 889e71d..cd2c211 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp @@ -266,6 +266,69 @@ TSharedRef 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 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 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 UPS_Editor_MainWidget::BuildSceneButtons() ] + SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center) [ - SAssignNew(BaseLevelCombo, SComboBox>) - .OptionsSource(&BaseLevelOptions) - .Method(EPopupMethod::UseCurrentWindow) - .OnSelectionChanged_Lambda([this](TSharedPtr 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(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 Item) -> TSharedRef - { - 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(GetOwningPlayer())) { BaseLevelOptions.Add(MakeShared(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& 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& 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 CurrentRow; + int32 ColIndex = 0; + + for (const TSharedPtr& 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 Card = SNew(SVerticalBox); + + // Thumbnail + TObjectPtr* FoundThumb = BaseLevelThumbnails.Find(Name); + if (FoundThumb && *FoundThumb) + { + TSharedPtr Brush = MakeShared(); + Brush->SetResourceObject(*FoundThumb); + Brush->ImageSize = FVector2D(CardWidth, CardHeight); + Brush->DrawAs = ESlateBrushDrawType::Image; + Brush->Tiling = ESlateBrushTileType::NoTile; + CachedBrushes.Add(Brush); + + TWeakPtr 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(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()) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h index 1a28627..356ebc5 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h @@ -77,14 +77,23 @@ private: // Scene save/load TSharedPtr SaveNameField; TSharedPtr SceneListContainer; - TSharedPtr>> BaseLevelCombo; TArray> BaseLevelOptions; - TSharedPtr BaseLevelComboText; + TSharedPtr BaseLevelButtonText; FString CurrentLevelFilter; /** Maps DisplayName → full MapPath for base levels (from MasterCatalog). */ TMap BaseLevelPathMap; + /** Maps DisplayName → Thumbnail texture for base levels. */ + TMap> BaseLevelThumbnails; TArray> CachedBrushes; + // BaseLevel selection popup + TSharedPtr BaseLevelPopup; + TSharedPtr BaseLevelGridContainer; + FString PendingBaseLevelSelection; + void OpenBaseLevelPopup(); + void ConfirmBaseLevelSelection(); + void CancelBaseLevelPopup(); + // Transform fields TSharedPtr LocX, LocY, LocZ; TSharedPtr RotX, RotY, RotZ;