Skip to content

File CollisionDataDetails.cpp

File List > CollisionEditor > Private > CollisionDataDetails.cpp

Go to the documentation of this file

#include "CollisionDataDetails.h"
#include "DetailCategoryBuilder.h"
#include "DetailLayoutBuilder.h"
#include "DetailWidgetRow.h"
#include "GameplayTagsEditorModule.h"
#include "PropertyHandle.h"
#include "ScopedTransaction.h"
#include "Data/CollisionData.h"
#include "Styling/AppStyle.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Layout/SBox.h"
#include "Widgets/Layout/SUniformGridPanel.h"
#include "Widgets/Text/STextBlock.h"

#define LOCTEXT_NAMESPACE "NightSkyCollisionEditor"

TSharedRef<IDetailCustomization> FCollisionDataDetails::MakeInstance()
{
    return MakeShareable(new FCollisionDataDetails);
}

void FCollisionDataDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
{
    CachedDetailBuilder = &DetailBuilder;

    TArray<TWeakObjectPtr<UObject>> ObjectsBeingCustomized;
    DetailBuilder.GetObjectsBeingCustomized(ObjectsBeingCustomized);

    if (ObjectsBeingCustomized.Num() != 1)
    {
        return;
    }

    CollisionDataPtr = Cast<UCollisionData>(ObjectsBeingCustomized[0].Get());
    if (!CollisionDataPtr.IsValid())
    {
        return;
    }

    CollisionFramesHandle = DetailBuilder.GetProperty(
        GET_MEMBER_NAME_CHECKED(UCollisionData, CollisionFrames));

    // Hide the default array display
    DetailBuilder.HideProperty(CollisionFramesHandle);

    // Also hide the transient editor property
    DetailBuilder.HideProperty(GET_MEMBER_NAME_CHECKED(UCollisionData, EditorSelectedIndex));

    IDetailCategoryBuilder& Category = DetailBuilder.EditCategory(
        "Selected Cel",
        LOCTEXT("SelectedCelCategory", "Selected Cel"),
        ECategoryPriority::Important);

    BuildSelectedCelView(DetailBuilder, Category);
}

void FCollisionDataDetails::BuildSelectedCelView(IDetailLayoutBuilder& DetailBuilder, IDetailCategoryBuilder& Category)
{
    UCollisionData* CollisionData = CollisionDataPtr.Get();
    if (!CollisionData)
    {
        return;
    }

    // Add "Add New Cel" button at the top
    Category.AddCustomRow(LOCTEXT("AddCelRow", "Add Cel"))
        .WholeRowContent()
        .HAlign(HAlign_Center)
        .VAlign(VAlign_Center)
        [
            SNew(SHorizontalBox)
            + SHorizontalBox::Slot()
            .AutoWidth()
            [
                SNew(SButton)
                .Text(LOCTEXT("AddNewCel", "Add New Cel"))
                .HAlign(HAlign_Center)
                .VAlign(VAlign_Center)
                .OnClicked(this, &FCollisionDataDetails::OnAddNewCel)
            ]
            + SHorizontalBox::Slot()
            .AutoWidth()
            .Padding(2.f)
            [
                SNew(SButton)
                .Text(LOCTEXT("DeleteSelectedCel", "Delete Selected"))
                .HAlign(HAlign_Center)
                .VAlign(VAlign_Center)
                .IsEnabled(this, &FCollisionDataDetails::CanDeleteSelectedCel)
                .OnClicked(this, &FCollisionDataDetails::OnDeleteSelectedCel)
            ]
        ];

    Category.AddCustomRow(LOCTEXT("TemplateSelectedCelRow", "New Cel From Selected"))
        .WholeRowContent()
        [
            SNew(SButton)
            .Text(LOCTEXT("NewCelFromSelected", "New Cel From Selected"))
            .VAlign(VAlign_Center)
            .HAlign(HAlign_Center)
            .IsEnabled(this, &FCollisionDataDetails::CanTemplateSelectedCel)
            .OnClicked(this, &FCollisionDataDetails::OnTemplateSelectedCel)
        ];

    Category.AddCustomRow(LOCTEXT("RefreshTreeRow", "Refresh Tree"))
        .WholeRowContent()
        [
            SNew(SButton)
            .HAlign(HAlign_Center)
            .VAlign(VAlign_Center)
            .Text(LOCTEXT("RefreshTree", "Refresh Tree"))
            .VAlign(VAlign_Center)
            .HAlign(HAlign_Center)
            .OnClicked(this, &FCollisionDataDetails::OnRefreshTree)
        ];

#if WITH_EDITORONLY_DATA
    const int32 SelectedIndex = CollisionData->EditorSelectedIndex;

    if (SelectedIndex == INDEX_NONE || !CollisionData->CollisionFrames.IsValidIndex(SelectedIndex))
    {
        Category.AddCustomRow(LOCTEXT("NoSelectionRow", "No Selection"))
            .WholeRowContent()
            [
                SNew(SBox)
                .Padding(FMargin(8.f, 16.f))
                [
                    SNew(STextBlock)
                    .Text(LOCTEXT("SelectCelPrompt", "Select a cel from the tree to edit its properties"))
                    .Font(FAppStyle::GetFontStyle("NormalFont"))
                    .ColorAndOpacity(FSlateColor::UseSubduedForeground())
                ]
            ];
        return;
    }

    const FCollisionStruct& SelectedCel = CollisionData->CollisionFrames[SelectedIndex];

    // Show header with cel name
    Category.AddCustomRow(LOCTEXT("CelNameRow", "Tree View Name"))
        .NameContent()
        [
            SNew(STextBlock)
            .Text(LOCTEXT("CelNameLabel", "Registered Cel Name"))
            .Font(FAppStyle::GetFontStyle("NormalFontBold"))
        ]
        .ValueContent()
        [
            SNew(STextBlock)
            .Text(FText::FromString(SelectedCel.CelName.ToString()))
            .Font(FAppStyle::GetFontStyle("NormalFont"))
        ];

    // Get the property handle for this specific array element
    TSharedPtr<IPropertyHandle> ElementHandle = CollisionFramesHandle->GetChildHandle(SelectedIndex);
    if (!ElementHandle.IsValid())
    {
        return;
    }

    // Add all child properties
    uint32 NumChildren = 0;
    ElementHandle->GetNumChildren(NumChildren);

    for (uint32 ChildIdx = 0; ChildIdx < NumChildren; ++ChildIdx)
    {
        TSharedPtr<IPropertyHandle> ChildHandle = ElementHandle->GetChildHandle(ChildIdx);
        if (ChildHandle.IsValid())
        {
            // Show all properties including CelName (it's editable here)
            Category.AddProperty(ChildHandle);
        }
    }
#endif
}

FReply FCollisionDataDetails::OnAddNewCel()
{
    UCollisionData* CollisionData = CollisionDataPtr.Get();
    if (!CollisionData)
    {
        return FReply::Handled();
    }

    FScopedTransaction Transaction(LOCTEXT("AddNewCelTransaction", "Add New Cel"));
    CollisionData->Modify();

    FCollisionStruct NewCel;
    CollisionData->CollisionFrames.Add(NewCel);

#if WITH_EDITORONLY_DATA
    // Select the new cel
    CollisionData->EditorSelectedIndex = CollisionData->CollisionFrames.Num() - 1;
    CollisionData->NotifyCollisionFramesChanged();
#endif

    if (CachedDetailBuilder)
    {
        CachedDetailBuilder->ForceRefreshDetails();
    }

    return FReply::Handled();
}

FReply FCollisionDataDetails::OnDeleteSelectedCel()
{
    UCollisionData* CollisionData = CollisionDataPtr.Get();
    if (!CollisionData)
    {
        return FReply::Handled();
    }

#if WITH_EDITORONLY_DATA
    const int32 SelectedIndex = CollisionData->EditorSelectedIndex;
    if (SelectedIndex == INDEX_NONE || !CollisionData->CollisionFrames.IsValidIndex(SelectedIndex))
    {
        return FReply::Handled();
    }

    FScopedTransaction Transaction(LOCTEXT("DeleteCelTransaction", "Delete Cel"));
    CollisionData->Modify();

    CollisionData->CollisionFrames.RemoveAt(SelectedIndex);

    // Adjust selection
    if (CollisionData->CollisionFrames.Num() == 0)
    {
        CollisionData->EditorSelectedIndex = INDEX_NONE;
    }
    else if (SelectedIndex >= CollisionData->CollisionFrames.Num())
    {
        CollisionData->EditorSelectedIndex = CollisionData->CollisionFrames.Num() - 1;
    }

    CollisionData->NotifyCollisionFramesChanged();

    if (CachedDetailBuilder)
    {
        CachedDetailBuilder->ForceRefreshDetails();
    }
#endif

    return FReply::Handled();
}

FReply FCollisionDataDetails::OnTemplateSelectedCel()
{
    UCollisionData* CollisionData = CollisionDataPtr.Get();
    if (!CollisionData)
    {
        return FReply::Handled();
    }

#if WITH_EDITORONLY_DATA
    const int32 SelectedIndex = CollisionData->EditorSelectedIndex;
    if (SelectedIndex == INDEX_NONE || !CollisionData->CollisionFrames.IsValidIndex(SelectedIndex))
    {
        return FReply::Handled();
    }

    const FCollisionStruct& SourceCel = CollisionData->CollisionFrames[SelectedIndex];

    // Default name: same as original but without the ending index (numbers) portion of the tag
    FGameplayTag NewCelDefaultName;
    if (SourceCel.CelName.IsValid())
    {
        FString TagString = SourceCel.CelName.ToString();
        int32 LastPeriodIndex;
        TagString.FindLastChar('.', LastPeriodIndex);
        if (LastPeriodIndex != INDEX_NONE)
        {
            // If the last portion of the tag is numeric, we use its parent as the default name
            auto LastTagString = TagString.RightChop(TagString.Len() - LastPeriodIndex);
            if (LastTagString.IsNumeric())
            {
                NewCelDefaultName = SourceCel.CelName.RequestDirectParent();
            }
            else
            {
                NewCelDefaultName = SourceCel.CelName;
            }
        }
        else
        {
            NewCelDefaultName = SourceCel.CelName;
        }
    }

    // ---- Dialog window for selecting the new tag name ----

    // Store result
    bool bUserConfirmed = false;

    const FVector2D InitialSize(450.0f, 500.0f);

    FWindowSizeLimits WindowSizeLimits;
    WindowSizeLimits.SetMinWidth(InitialSize.X).SetMinHeight(InitialSize.Y);

    // Need a shared ptr for the window so we can close it from button handlers
    TSharedRef<SWindow> PickerWindow = SNew(SWindow)
        .Title(LOCTEXT("PickCelTagTitle", "Pick Cel Tag"))
        .ClientSize(InitialSize)
        .SupportsMinimize(false)
        .SupportsMaximize(false)
        .SizingRule(ESizingRule::Autosized);

    PickerWindow->SetSizeLimits(WindowSizeLimits);

    // Build tag widget
    // IGameplayTagsEditorModule::Get invokes LoadModuleChecked
    IGameplayTagsEditorModule& TagsEditorModule = IGameplayTagsEditorModule::Get();

    // Editable single-tag field
    TSharedPtr<FGameplayTag> EditableNewName = MakeShared<FGameplayTag>(NewCelDefaultName);

    // Build widget
    TSharedRef<SWidget> TagWidget =
        TagsEditorModule.MakeGameplayTagWidget(
            FOnSetGameplayTag::CreateLambda(
                [&EditableNewName](FGameplayTag InTag)
                {
                    *EditableNewName = InTag;
                }),
            EditableNewName,
            FString("") // TODO: second button with filtering
        );

    // Compose window content with tag widget + OK/Cancel
    PickerWindow->SetContent(
        SNew(SBorder)
        .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder"))
        [
            SNew(SVerticalBox)

            + SVerticalBox::Slot()
            .AutoHeight()
            .Padding(8.0f)
            [
                SNew(STextBlock)
                .Text(LOCTEXT("PickCelTagLabel", "Select a new cel tag"))
            ]

            + SVerticalBox::Slot()
            .FillHeight(1.0f)
            .Padding(8.0f)
            [
                TagWidget
            ]

            + SVerticalBox::Slot()
            .AutoHeight()
            .HAlign(HAlign_Right)
            .Padding(8.0f)
            [
                SNew(SUniformGridPanel)
                .SlotPadding(FMargin(2.0f))

                + SUniformGridPanel::Slot(0, 0)
                [
                    SNew(SButton)
                    .Text(LOCTEXT("OK", "OK"))
                    .HAlign(HAlign_Center)
                    .VAlign(VAlign_Center)
                    .OnClicked_Lambda(
                        [&PickerWindow, &bUserConfirmed]()
                        {
                            bUserConfirmed = true;
                            FSlateApplication::Get().RequestDestroyWindow(PickerWindow);
                            return FReply::Handled();
                        })
                ]

                + SUniformGridPanel::Slot(1, 0)
                [
                    SNew(SButton)
                    .Text(LOCTEXT("Cancel", "Cancel"))
                    .HAlign(HAlign_Center)
                    .VAlign(VAlign_Center)
                    .OnClicked_Lambda(
                        [&PickerWindow]()
                        {
                            FSlateApplication::Get().RequestDestroyWindow(PickerWindow);
                            return FReply::Handled();
                        })
                ]
            ]
        ]);

    // This call BLOCKS until the modal window is closed
    FSlateApplication::Get().AddModalWindow(PickerWindow, nullptr);

    if (bUserConfirmed && EditableNewName.IsValid() && EditableNewName->IsValid())
    {
        // Re-validate after the modal dialog closes in case the array changed
        if (!CollisionData->CollisionFrames.IsValidIndex(SelectedIndex))
        {
            return FReply::Handled();
        }

        FScopedTransaction Transaction(LOCTEXT("TemplateSelectedCelTransaction", "New Cel From Selected"));
        CollisionData->Modify();

        FCollisionStruct NewCel = CollisionData->CollisionFrames[SelectedIndex];
        NewCel.CelName = *EditableNewName;

        const int32 NewIndex = CollisionData->CollisionFrames.Add(NewCel);
        CollisionData->EditorSelectedIndex = NewIndex;

        // Notify tree view and refresh the details panel to prevent stale property handles
        CollisionData->NotifyCollisionFramesChanged();

        if (CachedDetailBuilder)
        {
            CachedDetailBuilder->ForceRefreshDetails();
        }
    }
#endif

    return FReply::Handled();
}

bool FCollisionDataDetails::CanTemplateSelectedCel() const
{
#if WITH_EDITORONLY_DATA
    UCollisionData* CollisionData = CollisionDataPtr.Get();
    if (CollisionData)
    {
        const int32 SelectedIndex = CollisionData->EditorSelectedIndex;
        return SelectedIndex != INDEX_NONE && CollisionData->CollisionFrames.IsValidIndex(SelectedIndex);
    }
#endif
    return false;
}

bool FCollisionDataDetails::CanDeleteSelectedCel() const
{
#if WITH_EDITORONLY_DATA
    UCollisionData* CollisionData = CollisionDataPtr.Get();
    if (CollisionData)
    {
        const int32 SelectedIndex = CollisionData->EditorSelectedIndex;
        return SelectedIndex != INDEX_NONE && CollisionData->CollisionFrames.IsValidIndex(SelectedIndex);
    }
#endif
    return false;
}

FReply FCollisionDataDetails::OnRefreshTree()
{
    UCollisionData* CollisionData = CollisionDataPtr.Get();
    if (!CollisionData)
    {
        return FReply::Handled();
    }

#if WITH_EDITORONLY_DATA
    CollisionData->NotifyCollisionFramesChanged();
#endif

    if (CachedDetailBuilder)
    {
        CachedDetailBuilder->ForceRefreshDetails();
    }

    return FReply::Handled();
}

void FCollisionDataDetails::OnCelSelected(const FGameplayTag& CelName)
{
    SelectedCelName = CelName;
    if (CachedDetailBuilder)
    {
        CachedDetailBuilder->ForceRefreshDetails();
    }
}

#undef LOCTEXT_NAMESPACE