Problem: reproduce part of the UI from the Details panel in C++ (Image 1).
Image 1:
Full Details panel for reference:
Instead of the “delete all” button, I would like to have a delete button for each entry, like in the following Details panel:
Question: how can I re-use the UI from the editor to create a window with the elements shown above? The purpose is to enable the user to create pairs of labels-colors that can later be accessed from Blueprint. I’m using UE 5.3.
Here is what has been achieved so far:
Code
SlateWindowLabels.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Slate.h"
#include "SlateWindowLabels.generated.h"
/**
*
*/
UCLASS()
class POINTANNOTATIONTOOL_API ASlateWindowLabels : public AActor
{
GENERATED_BODY()
public:
ASlateWindowLabels();
UFUNCTION(BlueprintCallable, Category = "LabelCreation")
void OpenLabelDialog();
UFUNCTION(BlueprintCallable, Category = "LabelCreation")
TMap<FString, FLinearColor> GetLabelEntries() const;
private:
TSharedPtr<class SSlateWidgetLabels> WidgetLabels;
TSharedPtr<SWindow> Window;
};
SlateWindowLabels.cpp
#include "SlateWindowLabels.h"
#include "SSlateWidgetLabels.h"
ASlateWindowLabels::ASlateWindowLabels()
{
}
void ASlateWindowLabels::OpenLabelDialog()
{
if (GEngine)
{
if (GEngine->GameViewport)
{
Window = SNew(SWindow)
.ClientSize(FVector2D(800, 500))
.Title(FText::FromString(TEXT("Labels")))
[
SAssignNew(WidgetLabels, SSlateWidgetLabels)
];
FSlateApplication::Get().AddWindow(Window.ToSharedRef());
}
}
}
TMap<FString, FLinearColor> ASlateWindowLabels::GetLabelEntries() const
{
if (WidgetLabels.IsValid())
{
return WidgetLabels->GetEntries();
}
return TMap<FString, FLinearColor>();
}
SSlateWidgetLabels.h
#pragma once
#include "CoreMinimal.h"
#include "Widgets/SCompoundWidget.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#include "Widgets/Input/SEditableTextBox.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Colors/SColorPicker.h"
#include "Widgets/Layout/SBox.h"
#include "Widgets/Views/SListView.h"
class POINTANNOTATIONTOOL_API SSlateWidgetLabels : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SSlateWidgetLabels)
{}
SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
TMap<FString, FLinearColor> GetEntries() const
{
return Entries;
}
private:
// TMap to store the entries
TMap<FString, FLinearColor> Entries;
// Array of shared pointers to display the map entries in the list view
TArray<TSharedPtr<FString>> EntryKeys;
TSharedPtr<SListView<TSharedPtr<FString>>> ListView;
// Function to generate a row for each entry
TSharedRef<ITableRow> OnGenerateRow(TSharedPtr<FString> InKey, const TSharedRef<STableViewBase>& OwnerTable);
// Function to add a new entry
void AddEntry();
// Function to remove an existing entry
void RemoveEntry(const FString& Key);
void PrintEntriesToLog();
};
SSlateWidgetLabels.cpp
#include "SSlateWidgetLabels.h"
#include "Widgets/Text/STextBlock.h"
#include "SlateOptMacros.h"
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlateWidgetLabels::Construct(const FArguments& InArgs)
{
// Add initial entries to the map
Entries.Add("Label 1", FLinearColor::Red);
// Populate the EntryKeys array with keys from the map
for (const auto& Entry : Entries)
{
EntryKeys.Add(MakeShareable(new FString(Entry.Key)));
}
// Set up the widget layout
ChildSlot
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(SButton)
.Text(FText::FromString("Add Label"))
.OnClicked_Lambda([this]() -> FReply {
AddEntry();
return FReply::Handled();
})
]
+ SVerticalBox::Slot()
.FillHeight(1.0f)
[
SAssignNew(ListView, SListView<TSharedPtr<FString>>)
.ItemHeight(24)
.ListItemsSource(&EntryKeys)
.OnGenerateRow(this, &SSlateWidgetLabels::OnGenerateRow)
]
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(SButton)
.Text(FText::FromString("Next"))
.OnClicked_Lambda([this]() -> FReply {
PrintEntriesToLog();
return FReply::Handled();
})
]
];
}
void SSlateWidgetLabels::PrintEntriesToLog()
{
for (const auto& Entry : Entries)
{
UE_LOG(LogTemp, Log, TEXT("Label: %s, Color: %s"), *Entry.Key, *Entry.Value.ToString());
}
}
TSharedRef<ITableRow> SSlateWidgetLabels::OnGenerateRow(TSharedPtr<FString> InKey, const TSharedRef<STableViewBase>& OwnerTable)
{
FString Key = *InKey;
FLinearColor Value = Entries[Key];
return SNew(STableRow<TSharedPtr<FString>>, OwnerTable)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SEditableTextBox)
.Text(FText::FromString(Key))
.OnTextCommitted_Lambda([this, InKey](const FText& NewText, ETextCommit::Type CommitType) {
FString NewKey = NewText.ToString();
if (NewKey != *InKey)
{
FLinearColor Color = Entries[*InKey];
Entries.Remove(*InKey);
Entries.Add(NewKey, Color);
*InKey = NewKey;
ListView->RequestListRefresh();
}
})
]
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SColorPicker)
.OnColorCommitted_Lambda([this, InKey](FLinearColor NewColor) {
Entries[*InKey] = NewColor;
})
]
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SButton)
.Text(FText::FromString("Remove"))
.OnClicked_Lambda([this, InKey]() -> FReply {
RemoveEntry(*InKey);
return FReply::Handled();
})
]
];
}
void SSlateWidgetLabels::AddEntry()
{
FString NewKey = "Label";
int32 Suffix = 1;
while (Entries.Contains(NewKey))
{
NewKey = FString::Printf(TEXT("Label %d"), Suffix++);
}
Entries.Add(NewKey, FLinearColor::White);
EntryKeys.Add(MakeShareable(new FString(NewKey)));
ListView->RequestListRefresh();
}
void SSlateWidgetLabels::RemoveEntry(const FString& Key)
{
Entries.Remove(Key);
EntryKeys.RemoveAll([&Key](const TSharedPtr<FString>& EntryKey) {
return *EntryKey == Key;
});
ListView->RequestListRefresh();
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
Sorry for the long post and thank you for your time!