Files
UnrealEngine/Engine/Plugins/Runtime/SmartObjects/Source/SmartObjectsEditorModule/Private/SmartObjectAssetToolkit.cpp
2025-05-18 13:04:45 +08:00

1055 lines
33 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SmartObjectAssetToolkit.h"
#include "AssetEditorModeManager.h"
#include "Engine/StaticMesh.h"
#include "ScopedTransaction.h"
#include "SmartObjectAssetEditorViewportClient.h"
#include "SmartObjectDefinition.h"
#include "SmartObjectTypes.h"
#include "SmartObjectViewModel.h"
#include "Viewports.h"
#include "Widgets/Docking/SDockTab.h"
#include "Widgets/Layout/SScrollBox.h"
#include "SSmartObjectViewport.h"
#include "IStructureDetailsView.h"
#include "Modules/ModuleManager.h"
#include "PropertyEditorModule.h"
#include "Widgets/Views/STableViewBase.h"
#include "Widgets/Views/SListView.h"
#include "Brushes/SlateRoundedBoxBrush.h"
#include "SPositiveActionButton.h"
#include "PropertyPath.h"
#include "PropertyBindingExtension.h"
#define LOCTEXT_NAMESPACE "SmartObjectAssetToolkit"
const FName FSmartObjectAssetToolkit::PreviewSettingsTabID(TEXT("SmartObjectAssetToolkit_Preview"));
const FName FSmartObjectAssetToolkit::OutlinerTabID(TEXT("SmartObjectAssetToolkit_Outliner"));
const FName FSmartObjectAssetToolkit::SceneViewportTabID(TEXT("SmartObjectAssetToolkit_Viewport"));
const FName FSmartObjectAssetToolkit::DetailsTabID(TEXT("SmartObjectAssetToolkit_Details"));
/** Outliner item type. */
enum class ESmartObjectSlotItemType : uint8
{
Object,
ObjectDefinitionData,
Slot,
SlotDefinitionData,
};
/** Struct used to describe an item in the outliner. */
struct FSmartObjectOutlinerItem : public TSharedFromThis<FSmartObjectOutlinerItem>
{
bool IsAllowedToDrag() const { return Type == ESmartObjectSlotItemType::Slot; }
bool operator==(const FSmartObjectOutlinerItem& RHS) const { return ID == RHS.ID; }
FGuid ID;
ESmartObjectSlotItemType Type = ESmartObjectSlotItemType::Object;
TWeakPtr<FSmartObjectOutlinerItem> Parent;
TArray<TSharedPtr<FSmartObjectOutlinerItem>> ChildItems;
};
/** Outliner drag and drop operation. */
class FSmartObjectItemDragDropOp : public FDecoratedDragDropOp
{
public:
DRAG_DROP_OPERATOR_TYPE(FSmartObjectItemDragDropOp, FDecoratedDragDropOp)
static TSharedRef<FSmartObjectItemDragDropOp> New(TWeakPtr<FSmartObjectOutlinerItem> InItem, const FText InDescription)
{
TSharedRef<FSmartObjectItemDragDropOp> Operation = MakeShared<FSmartObjectItemDragDropOp>();
Operation->Item = InItem;
Operation->Description = InDescription;
Operation->Construct();
return Operation;
}
virtual TSharedPtr<SWidget> GetDefaultDecorator() const override
{
return SNew(SBorder)
.Visibility(EVisibility::Visible)
.BorderImage(FAppStyle::GetBrush("Menu.Background"))
[
SNew(STextBlock)
.Text(Description)
];
}
TWeakPtr<FSmartObjectOutlinerItem> Item;
FText Description;
};
namespace UE::SmartObject::Editor
{
TSharedPtr<FSmartObjectOutlinerItem> FindItem(const FGuid ItemToFind, TConstArrayView<TSharedPtr<FSmartObjectOutlinerItem>> Items)
{
for (const TSharedPtr<FSmartObjectOutlinerItem>& Item : Items)
{
if (Item.IsValid())
{
if (Item->ID == ItemToFind)
{
return Item;
}
const TSharedPtr<FSmartObjectOutlinerItem> ChildItem = FindItem(ItemToFind, Item->ChildItems);
if (ChildItem.IsValid())
{
return ChildItem;
}
}
}
return nullptr;
}
void FlattenItemList(TArray<TSharedPtr<FSmartObjectOutlinerItem>>& InItems, TArray<TSharedPtr<FSmartObjectOutlinerItem>>& OutItems)
{
for (TSharedPtr<FSmartObjectOutlinerItem>& Item : InItems)
{
if (Item.IsValid())
{
if (Item->ChildItems.Num() > 0)
{
FlattenItemList(Item->ChildItems, OutItems);
}
OutItems.Add(Item);
}
}
InItems.Reset();
}
}; // UE::SmartObject::Editor
//----------------------------------------------------------------------//
// FSmartObjectAssetToolkit
//----------------------------------------------------------------------//
FSmartObjectAssetToolkit::FSmartObjectAssetToolkit(UAssetEditor* InOwningAssetEditor)
: FBaseAssetToolkit(InOwningAssetEditor)
{
FPreviewScene::ConstructionValues PreviewSceneArgs;
AdvancedPreviewScene = MakeUnique<FAdvancedPreviewScene>(PreviewSceneArgs);
// Apply small Z offset to not hide the grid
constexpr float DefaultFloorOffset = 1.0f;
AdvancedPreviewScene->SetFloorOffset(DefaultFloorOffset);
// Setup our default layout
StandaloneDefaultLayout = FTabManager::NewLayout(FName("SmartObjectAssetEditorLayout3"))
->AddArea
(
FTabManager::NewPrimaryArea()
->SetOrientation(Orient_Vertical)
->Split
(
FTabManager::NewSplitter()
->SetOrientation(Orient_Horizontal)
->Split
(
FTabManager::NewStack()
->SetSizeCoefficient(0.7f)
->AddTab(SceneViewportTabID, ETabState::OpenedTab)
->SetHideTabWell(true)
)
->Split
(
FTabManager::NewSplitter()
->SetSizeCoefficient(0.3f)
->SetOrientation(Orient_Vertical)
->Split
(
FTabManager::NewStack()
->SetSizeCoefficient(0.3f)
->AddTab(OutlinerTabID, ETabState::OpenedTab)
->AddTab(PreviewSettingsTabID, ETabState::OpenedTab)
->SetForegroundTab(OutlinerTabID)
)
->Split
(
FTabManager::NewStack()
->SetSizeCoefficient(0.7f)
->AddTab(DetailsTabID, ETabState::OpenedTab)
)
)
)
);
}
FSmartObjectAssetToolkit::~FSmartObjectAssetToolkit()
{
if (ViewModel.IsValid())
{
ViewModel->Unregister();
}
SelectionChangedHandle.Reset();
SlotsChangedHandle.Reset();
}
TSharedPtr<FEditorViewportClient> FSmartObjectAssetToolkit::CreateEditorViewportClient() const
{
// Set our advanced preview scene in the EditorModeManager
StaticCastSharedPtr<FAssetEditorModeManager>(EditorModeManager)->SetPreviewScene(AdvancedPreviewScene.Get());
// Create and setup our custom viewport client
SmartObjectViewportClient = MakeShared<FSmartObjectAssetEditorViewportClient>(SharedThis(this), AdvancedPreviewScene.Get());
SmartObjectViewportClient->ViewportType = LVT_Perspective;
SmartObjectViewportClient->SetViewLocation(EditorViewportDefs::DefaultPerspectiveViewLocation);
SmartObjectViewportClient->SetViewRotation(EditorViewportDefs::DefaultPerspectiveViewRotation);
return SmartObjectViewportClient;
}
void FSmartObjectAssetToolkit::SetEditingObject(UObject* InObject)
{
// Override the default as we want to do our own selection of the details panel
}
void FSmartObjectAssetToolkit::PostInitAssetEditor()
{
USmartObjectDefinition* Definition = Cast<USmartObjectDefinition>(GetEditingObject());
check(Definition);
if (!ViewModel)
{
ViewModel = FSmartObjectViewModel::Register(Definition);
check(ViewModel.IsValid());
}
// Allow the viewport client to interact with the preview component
checkf(SmartObjectViewportClient.IsValid(), TEXT("ViewportClient is created in CreateEditorViewportClient before calling PostInitAssetEditor"));
SmartObjectViewportClient->SetSmartObjectDefinition(*Definition);
SmartObjectViewportClient->SetViewModel(ViewModel);
if (PreviewDetailsView.IsValid())
{
CachedPreviewData = MakeShared<FStructOnScope>(TBaseStructure<FSmartObjectDefinitionPreviewData>::Get(), (uint8*)&Definition->PreviewData);
CachedPreviewData->SetPackage(Definition->GetPackage());
PreviewDetailsView->SetStructureData(CachedPreviewData);
}
UpdatePreviewActor();
UpdateItemList();
SelectionChangedHandle = ViewModel->GetOnSelectionChanged().AddSP(this, &FSmartObjectAssetToolkit::HandleSelectionChanged);
SlotsChangedHandle = ViewModel->GetOnSlotsChanged().AddSP(this, &FSmartObjectAssetToolkit::HandleSlotsChanged);
// Register to be notified when properties are edited
FCoreUObjectDelegates::OnObjectPropertyChanged.AddSP(this, &FSmartObjectAssetToolkit::OnPropertyChanged);
UE::SmartObject::Delegates::OnParametersChanged.AddSP(this, &FSmartObjectAssetToolkit::OnParametersChanged);
}
void FSmartObjectAssetToolkit::OnParametersChanged(const USmartObjectDefinition& SmartObjectDefinition)
{
if (ViewModel && ViewModel->GetAsset() == &SmartObjectDefinition)
{
// Accessible structs might be different after modifying parameters so forcing refresh
// so the FStateTreeBindingExtension can rebuild the list of bindable structs
if (DetailsAssetView.IsValid())
{
DetailsAssetView->ForceRefresh();
}
}
}
void FSmartObjectAssetToolkit::RegisterTabSpawners(const TSharedRef<FTabManager>& InTabManager)
{
FAssetEditorToolkit::RegisterTabSpawners(InTabManager);
if (!AssetEditorTabsCategory.IsValid())
{
// Use the first child category of the local workspace root if there is one, otherwise use the root itself
const TArray<TSharedRef<FWorkspaceItem>>& LocalCategories = InTabManager->GetLocalWorkspaceMenuRoot()->GetChildItems();
AssetEditorTabsCategory = LocalCategories.Num() > 0 ? LocalCategories[0] : InTabManager->GetLocalWorkspaceMenuRoot();
}
InTabManager->RegisterTabSpawner(DetailsTabID, FOnSpawnTab::CreateSP(this, &FSmartObjectAssetToolkit::SpawnTab_SelectionDetails))
.SetDisplayName(LOCTEXT("Details", "Details"))
.SetGroup(AssetEditorTabsCategory.ToSharedRef())
.SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Tabs.Details"));
InTabManager->RegisterTabSpawner(SceneViewportTabID, FOnSpawnTab::CreateSP(this, &FSmartObjectAssetToolkit::SpawnTab_SceneViewport))
.SetDisplayName(LOCTEXT("Viewport", "Viewport"))
.SetGroup(AssetEditorTabsCategory.ToSharedRef())
.SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Tabs.Viewports"));
InTabManager->RegisterTabSpawner(OutlinerTabID, FOnSpawnTab::CreateSP(this, &FSmartObjectAssetToolkit::SpawnTab_Outliner))
.SetDisplayName(LOCTEXT("Outliner", "Outliner"))
.SetGroup(AssetEditorTabsCategory.ToSharedRef())
.SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Tabs.Outliner"));
InTabManager->RegisterTabSpawner(PreviewSettingsTabID, FOnSpawnTab::CreateSP(this, &FSmartObjectAssetToolkit::SpawnTab_PreviewSettings))
.SetDisplayName(LOCTEXT("PreviewSettings", "Preview Settings"))
.SetGroup(AssetEditorTabsCategory.ToSharedRef())
.SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Visibility"));
}
TSharedRef<SDockTab> FSmartObjectAssetToolkit::SpawnTab_SceneViewport(const FSpawnTabArgs& Args)
{
check(Args.GetTabId() == FSmartObjectAssetToolkit::SceneViewportTabID);
const TSharedRef<SSmartObjectViewport> ViewportWidget = SNew(SSmartObjectViewport)
.EditorViewportClient(StaticCastSharedPtr<FSmartObjectAssetEditorViewportClient>(ViewportClient))
.AssetEditorToolkit(StaticCastSharedRef<FSmartObjectAssetToolkit>(AsShared()))
.PreviewScene(AdvancedPreviewScene.Get());
TSharedRef<SDockTab> SpawnedTab = SNew(SDockTab).Label(LOCTEXT("ViewportTab_Title", "Viewport"));
SpawnedTab->SetContent(ViewportWidget);
return SpawnedTab;
}
TSharedRef<SDockTab> FSmartObjectAssetToolkit::SpawnTab_Outliner(const FSpawnTabArgs& Args)
{
UpdateItemList();
const TSharedPtr<SDockTab> OutlinerTab = SNew(SDockTab)
.Label(LOCTEXT("OutlinerTitle", "Outliner"))
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(2)
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
.Padding(FMargin(0,0,4,0))
[
SNew(SPositiveActionButton)
.Icon(FAppStyle::Get().GetBrush("Icons.Plus"))
.Text(LOCTEXT("AddSlot", "Add Slot"))
.OnClicked_Lambda([this]()
{
if (ViewModel.IsValid())
{
const FGuid NewSlotID = ViewModel->AddSlot(FGuid());
ViewModel->SetSelection({ NewSlotID });
return FReply::Handled();
}
return FReply::Unhandled();
})
]
]
+ SVerticalBox::Slot()
.FillHeight(1)
.Padding(2)
[
SAssignNew(ItemTreeWidget, STreeView<TSharedPtr<FSmartObjectOutlinerItem>>)
.TreeItemsSource(&ItemList)
.OnGenerateRow(this, &FSmartObjectAssetToolkit::OnGenerateRow)
.OnGetChildren(this, &FSmartObjectAssetToolkit::OnGetChildren)
.OnSelectionChanged(this, &FSmartObjectAssetToolkit::OnOutlinerSelectionChanged)
.OnContextMenuOpening(this, &FSmartObjectAssetToolkit::OnOutlinerContextMenu)
.SelectionMode(ESelectionMode::Multi)
]
];
return OutlinerTab.ToSharedRef();
}
TSharedRef<SDockTab> FSmartObjectAssetToolkit::SpawnTab_SelectionDetails(const FSpawnTabArgs& Args)
{
DetailsTab = SNew(SDockTab).Label(LOCTEXT("DetailsTitle", "Details"));
FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
FDetailsViewArgs DetailsViewArgs;
DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea;
DetailsViewArgs.bHideSelectionTip = true;
DetailsViewArgs.bAllowSearch = true;
DetailsAssetView = PropertyEditorModule.CreateDetailView(DetailsViewArgs);
DetailsAssetView->SetExtensionHandler(MakeShared<FPropertyBindingExtension>());
if (USmartObjectDefinition* Definition = CastChecked<USmartObjectDefinition>(GetEditingObject()))
{
if (!ViewModel)
{
ViewModel = FSmartObjectViewModel::Register(Definition);
check(ViewModel.IsValid());
}
DetailsAssetView->SetObject(Definition);
}
DetailsTab->SetContent(DetailsAssetView.ToSharedRef());
return DetailsTab.ToSharedRef();
}
void FSmartObjectAssetToolkit::UpdateDetailsSelection()
{
if (!DetailsTab.IsValid()
|| !ViewModel.IsValid())
{
return;
}
USmartObjectDefinition* Definition = ViewModel->GetAsset();
if (!Definition)
{
return;
}
// Find the type of last selection and required indices for finding the item.
bool bIsSelectedAsset = false;
int32 SelectedSlotIndex = INDEX_NONE;
int32 SelectedDefinitionDataIndex = INDEX_NONE;
const TConstArrayView<FGuid> Selection = ViewModel->GetSelection();
for (int32 Index = Selection.Num() - 1; Index >= 0; Index--)
{
const FGuid ItemID = Selection[Index];
// If the selected item is empty guid, we have selected the asset in the outliner view.
if (!ItemID.IsValid())
{
bIsSelectedAsset = true;
break;
}
// Find selected slot and definition data (if feasible) indices.
if (Definition->FindSlotAndDefinitionDataIndexByID(ItemID, SelectedSlotIndex, SelectedDefinitionDataIndex))
{
break;
}
}
FPropertyPath HighlightPath;
if (SelectedDefinitionDataIndex != INDEX_NONE)
{
// Selected definition data.
FArrayProperty* SlotsProperty = CastFieldChecked<FArrayProperty>(USmartObjectDefinition::StaticClass()->FindPropertyByName(GET_MEMBER_NAME_CHECKED(USmartObjectDefinition, Slots)));
HighlightPath.AddProperty(FPropertyInfo(SlotsProperty));
HighlightPath.AddProperty(FPropertyInfo(SlotsProperty->Inner, SelectedSlotIndex));
FArrayProperty* DefinitionDataProperty = CastFieldChecked<FArrayProperty>(TBaseStructure<FSmartObjectSlotDefinition>::Get()->FindPropertyByName(GET_MEMBER_NAME_CHECKED(FSmartObjectSlotDefinition, DefinitionData)));
HighlightPath.AddProperty(FPropertyInfo(DefinitionDataProperty));
HighlightPath.AddProperty(FPropertyInfo(DefinitionDataProperty->Inner, SelectedDefinitionDataIndex));
}
else if (SelectedSlotIndex != INDEX_NONE)
{
// Selected slot
FArrayProperty* SlotsProperty = CastFieldChecked<FArrayProperty>(USmartObjectDefinition::StaticClass()->FindPropertyByName(GET_MEMBER_NAME_CHECKED(USmartObjectDefinition, Slots)));
HighlightPath.AddProperty(FPropertyInfo(SlotsProperty));
HighlightPath.AddProperty(FPropertyInfo(SlotsProperty->Inner, SelectedSlotIndex));
}
else if (bIsSelectedAsset)
{
// Beginning of the asset
FArrayProperty* DefaultBehaviorDefinitionsProperty = CastFieldChecked<FArrayProperty>(USmartObjectDefinition::StaticClass()->FindPropertyByName(GET_MEMBER_NAME_CHECKED(USmartObjectDefinition, DefaultBehaviorDefinitions)));
HighlightPath.AddProperty(FPropertyInfo(DefaultBehaviorDefinitionsProperty));
}
if (DetailsAssetView.IsValid())
{
DetailsAssetView->ScrollPropertyIntoView(HighlightPath);
}
}
FText FSmartObjectAssetToolkit::GetOutlinerItemDescription(TSharedPtr<FSmartObjectOutlinerItem> Item) const
{
if (!Item.IsValid())
{
return FText::GetEmpty();
}
const USmartObjectDefinition* Definition = CastChecked<USmartObjectDefinition>(GetEditingObject());
if (!Definition)
{
return FText::GetEmpty();
}
if (Item->Type == ESmartObjectSlotItemType::Object)
{
return FText::FromString(Definition->GetName());
}
else if (Item->Type == ESmartObjectSlotItemType::ObjectDefinitionData)
{
const FSmartObjectDefinitionDataProxy* DataProxy = Definition->DefinitionData.FindByPredicate([&ID = Item->ID](const FSmartObjectDefinitionDataProxy& DataProxy)
{
return DataProxy.ID == ID;
});
if (DataProxy && DataProxy->Data.GetScriptStruct() != nullptr)
{
return DataProxy->Data.GetScriptStruct()->GetDisplayNameText();
}
return LOCTEXT("None", "None");
}
else if (Item->Type == ESmartObjectSlotItemType::Slot)
{
const int32 SlotIndex = Definition->FindSlotByID(Item->ID);
if (SlotIndex != INDEX_NONE)
{
const FSmartObjectSlotDefinition& SlotDefinition = Definition->GetSlot(SlotIndex);
return FText::FromName(SlotDefinition.Name);
}
}
else if (Item->Type == ESmartObjectSlotItemType::SlotDefinitionData)
{
int32 SlotIndex = INDEX_NONE;
int32 DefinitionDataIndex = INDEX_NONE;
if (Definition->FindSlotAndDefinitionDataIndexByID(Item->ID, SlotIndex, DefinitionDataIndex))
{
const FSmartObjectSlotDefinition& SlotDefinition = Definition->GetSlot(SlotIndex);
const FSmartObjectDefinitionDataProxy& DataProxy = SlotDefinition.DefinitionData[DefinitionDataIndex];
if (DataProxy.Data.GetScriptStruct() != nullptr)
{
return DataProxy.Data.GetScriptStruct()->GetDisplayNameText();
}
}
return LOCTEXT("None", "None");
}
return FText::GetEmpty();
}
FSlateColor FSmartObjectAssetToolkit::GetOutlinerItemColor(TSharedPtr<FSmartObjectOutlinerItem> Item) const
{
if (!Item.IsValid())
{
return FColor::Silver;
}
const USmartObjectDefinition* Definition = CastChecked<USmartObjectDefinition>(GetEditingObject());
if (!Definition)
{
return FColor::Silver;
}
if (Item->Type == ESmartObjectSlotItemType::Object)
{
return FColor::Silver;
}
else if (Item->Type == ESmartObjectSlotItemType::ObjectDefinitionData)
{
return FColor::Silver;
}
else if (Item->Type == ESmartObjectSlotItemType::Slot)
{
const int32 SlotIndex = Definition->FindSlotByID(Item->ID);
if (SlotIndex != INDEX_NONE)
{
const FSmartObjectSlotDefinition& Slot = Definition->GetSlot(SlotIndex);
return Slot.DEBUG_DrawColor;
}
return FColor::Silver;
}
else if (Item->Type == ESmartObjectSlotItemType::SlotDefinitionData)
{
return FColor::Silver;
}
return FColor::Silver;
}
TSharedRef<ITableRow> FSmartObjectAssetToolkit::OnGenerateRow(TSharedPtr<FSmartObjectOutlinerItem> InItem, const TSharedRef<STableViewBase>& OwnerTable)
{
return SNew(STableRow< TSharedPtr<FSmartObjectOutlinerItem> >, OwnerTable)
.ShowSelection(true)
.OnDragDetected(this, &FSmartObjectAssetToolkit::OnOutlinerDragDetected)
.OnCanAcceptDrop(this, &FSmartObjectAssetToolkit::OnOutlinerCanAcceptDrop)
.OnAcceptDrop(this, &FSmartObjectAssetToolkit::OnOutlinerAcceptDrop)
[
SNew(SBox)
.Padding(FMargin(0, 2))
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(5, 0, 0, 0)
[
SNew(SImage)
.Image(FCoreStyle::Get().GetBrush("VerticalBoxDragIndicatorShort"))
.Visibility_Lambda([InItem]()
{
return InItem->IsAllowedToDrag() ? EVisibility::Visible : EVisibility::Hidden;
})
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(5, 0, 0, 0)
[
SNew(SImage)
.Image_Lambda([InItem]() -> const FSlateBrush*
{
if (InItem->Type == ESmartObjectSlotItemType::Object)
{
return FAppStyle::Get().GetBrush("Icons.Settings");
}
if (InItem->Type == ESmartObjectSlotItemType::ObjectDefinitionData)
{
return FAppStyle::Get().GetBrush("SCS.Component");
}
if (InItem->Type == ESmartObjectSlotItemType::Slot)
{
return FAppStyle::Get().GetBrush("Icons.Transform");
}
if (InItem->Type == ESmartObjectSlotItemType::SlotDefinitionData)
{
return FAppStyle::Get().GetBrush("SCS.Component");
}
return nullptr;
})
.ColorAndOpacity(this, &FSmartObjectAssetToolkit::GetOutlinerItemColor, InItem)
]
+ SHorizontalBox::Slot()
.Padding(FMargin(10, 0, 0, 0))
[
SNew(STextBlock)
.Font_Lambda([InItem]() -> FSlateFontInfo
{
if (InItem->Type == ESmartObjectSlotItemType::Object)
{
return FCoreStyle::Get().GetFontStyle("BoldFont");
}
return FCoreStyle::Get().GetFontStyle("NormalFont");
})
.MinDesiredWidth(150)
.Text(this, &FSmartObjectAssetToolkit::GetOutlinerItemDescription, InItem)
.Justification(ETextJustify::Left)
]
]
];
}
void FSmartObjectAssetToolkit::OnGetChildren(TSharedPtr<FSmartObjectOutlinerItem> InItem, TArray<TSharedPtr<FSmartObjectOutlinerItem>>& OutChildren) const
{
OutChildren = InItem->ChildItems;
}
void FSmartObjectAssetToolkit::OnOutlinerSelectionChanged(TSharedPtr<FSmartObjectOutlinerItem> SelectedItem, ESelectInfo::Type SelectType)
{
if (bUpdatingOutlinerSelection
|| !ItemTreeWidget.IsValid()
|| !ViewModel.IsValid())
{
return;
}
TGuardValue<bool> UpdatingOutlinerSelectionGuard(bUpdatingViewSelection, true);
const TArray<TSharedPtr<FSmartObjectOutlinerItem>>& OutlinerSelectedItems = ItemTreeWidget->GetSelectedItems();
TArray<FGuid> SelectedItems;
for (const TSharedPtr<FSmartObjectOutlinerItem>& OutlinerItem : OutlinerSelectedItems)
{
if (OutlinerItem.IsValid())
{
SelectedItems.Add(OutlinerItem->ID);
}
}
ViewModel->SetSelection(SelectedItems);
UpdateDetailsSelection();
}
TSharedPtr<SWidget> FSmartObjectAssetToolkit::OnOutlinerContextMenu()
{
TArray<TSharedPtr<FSmartObjectOutlinerItem>> OutlinerSelectedItems = ItemTreeWidget->GetSelectedItems();
if (OutlinerSelectedItems.IsEmpty())
{
return {};
}
TSharedPtr<FSmartObjectOutlinerItem> OutlinerSelectedItem = OutlinerSelectedItems[0];
if (!OutlinerSelectedItem.IsValid())
{
return {};
}
const bool bIsSlot = OutlinerSelectedItem->Type == ESmartObjectSlotItemType::Slot;
FMenuBuilder MenuBuilder(/*bInShouldCloseWindowAfterMenuSelection*/true, /*InCommandList*/nullptr);
if (bIsSlot)
{
MenuBuilder.AddMenuEntry(
LOCTEXT("AddSlot", "Add Slot"),
FText::GetEmpty(),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Plus"),
FUIAction(
FExecuteAction::CreateLambda([ViewModel = ViewModel, SlotID = OutlinerSelectedItem->ID]()
{
if (ViewModel.IsValid())
{
ViewModel->AddSlot(SlotID);
}
}))
);
MenuBuilder.AddMenuEntry(
LOCTEXT("RemoveSlot", "Remove Slot"),
FText::GetEmpty(),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Delete"),
FUIAction(
FExecuteAction::CreateLambda([ViewModel = ViewModel, SlotID = OutlinerSelectedItem->ID]()
{
if (ViewModel.IsValid())
{
ViewModel->RemoveSlot(SlotID);
}
}))
);
}
return MenuBuilder.MakeWidget();
}
void FSmartObjectAssetToolkit::HandleSelectionChanged(TConstArrayView<FGuid> InSelection)
{
if (bUpdatingViewSelection)
{
return;
}
TGuardValue<bool> UpdatingOutlinerSelectionGuard(bUpdatingOutlinerSelection, true);
ItemTreeWidget->ClearSelection();
TArray<TSharedPtr<FSmartObjectOutlinerItem>> Selection;
Selection.Reserve(InSelection.Num());
for (const FGuid& Item : InSelection)
{
TSharedPtr<FSmartObjectOutlinerItem> OutlinerItem = UE::SmartObject::Editor::FindItem(Item, ItemList);
if (OutlinerItem.IsValid())
{
Selection.Add(OutlinerItem);
// Make sure selected is visible, by expanding parents.
TSharedPtr<FSmartObjectOutlinerItem> ExpandingOutlinerItem = OutlinerItem->Parent.Pin();
while (ExpandingOutlinerItem.IsValid())
{
ItemTreeWidget->SetItemExpansion(ExpandingOutlinerItem, true);
ExpandingOutlinerItem = ExpandingOutlinerItem->Parent.Pin();
}
}
}
ItemTreeWidget->SetItemSelection(Selection, true);
UpdateDetailsSelection();
}
void FSmartObjectAssetToolkit::HandleSlotsChanged(USmartObjectDefinition* Definition)
{
UpdateItemList();
UpdateDetailsSelection();
}
FReply FSmartObjectAssetToolkit::OnOutlinerDragDetected(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) const
{
const TArray<TSharedPtr<FSmartObjectOutlinerItem>> SelectedItems = ItemTreeWidget.Get()->GetSelectedItems();
if (SelectedItems.Num() != 1)
{
return FReply::Unhandled();
}
if (!SelectedItems[0]->IsAllowedToDrag())
{
return FReply::Unhandled();
}
if (MouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton))
{
const TSharedPtr<FSmartObjectOutlinerItem> DraggedItem = SelectedItems[0];
const TSharedRef<FSmartObjectItemDragDropOp> DragDropOp = FSmartObjectItemDragDropOp::New(DraggedItem, GetOutlinerItemDescription(DraggedItem));
return FReply::Handled().BeginDragDrop(DragDropOp);
}
return FReply::Unhandled();
}
TOptional<EItemDropZone> FSmartObjectAssetToolkit::OnOutlinerCanAcceptDrop(const FDragDropEvent& DragDropEvent, EItemDropZone DropZone, const TSharedPtr<FSmartObjectOutlinerItem> TargetItem) const
{
const USmartObjectDefinition* Definition = CastChecked<USmartObjectDefinition>(GetEditingObject());
if (!Definition)
{
return {};
}
const TSharedPtr<FSmartObjectItemDragDropOp> DragDropOp = DragDropEvent.GetOperationAs<FSmartObjectItemDragDropOp>();
if (!DragDropOp.IsValid())
{
return {};
}
const TSharedPtr<FSmartObjectOutlinerItem> SourceItem = DragDropOp->Item.Pin();
if (!SourceItem.IsValid() || !TargetItem.IsValid())
{
return {};
}
int32 SourceSlotIndex = INDEX_NONE;
int32 SourceDataDefinitionIndex = INDEX_NONE;
Definition->FindSlotAndDefinitionDataIndexByID(SourceItem->ID, SourceSlotIndex, SourceDataDefinitionIndex);
int32 TargetSlotIndex = INDEX_NONE;
int32 TargetDataDefinitionIndex = INDEX_NONE;
Definition->FindSlotAndDefinitionDataIndexByID(TargetItem->ID, TargetSlotIndex, TargetDataDefinitionIndex);
if (SourceItem->Type == ESmartObjectSlotItemType::Slot
&& (TargetItem->Type == ESmartObjectSlotItemType::Slot
|| TargetItem->Type == ESmartObjectSlotItemType::SlotDefinitionData))
{
if (TargetSlotIndex < SourceSlotIndex)
{
return EItemDropZone::AboveItem;
}
if (TargetSlotIndex > SourceSlotIndex)
{
return EItemDropZone::BelowItem;
}
}
return {};
}
FReply FSmartObjectAssetToolkit::OnOutlinerAcceptDrop(const FDragDropEvent& DragDropEvent, EItemDropZone DropZone, const TSharedPtr<FSmartObjectOutlinerItem> TargetItem)
{
if (!ViewModel.IsValid())
{
return FReply::Unhandled();
}
const TSharedPtr<FSmartObjectItemDragDropOp> DragDropOp = DragDropEvent.GetOperationAs<FSmartObjectItemDragDropOp>();
if (!DragDropOp.IsValid())
{
return FReply::Unhandled();
}
const TSharedPtr<FSmartObjectOutlinerItem> SourceItem = DragDropOp->Item.Pin();
if (!SourceItem.IsValid() || !TargetItem.IsValid())
{
return FReply::Unhandled();
}
ViewModel->MoveSlot(SourceItem->ID, TargetItem->ID);
UpdateCachedPreviewDataFromDefinition();
UpdateDetailsSelection();
return FReply::Handled();
}
TSharedRef<SDockTab> FSmartObjectAssetToolkit::SpawnTab_PreviewSettings(const FSpawnTabArgs& Args)
{
FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
FDetailsViewArgs DetailsViewArgs;
DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea;
FStructureDetailsViewArgs StructureViewArgs;
StructureViewArgs.bShowObjects = true;
StructureViewArgs.bShowAssets = true;
StructureViewArgs.bShowClasses = true;
StructureViewArgs.bShowInterfaces = true;
PreviewDetailsView = PropertyEditorModule.CreateStructureDetailView(DetailsViewArgs, StructureViewArgs, nullptr);
PreviewDetailsView->GetOnFinishedChangingPropertiesDelegate().AddLambda([&](const FPropertyChangedEvent& PropertyChangedEvent)
{
USmartObjectDefinition* Definition = Cast<USmartObjectDefinition>(GetEditingObject());
// Ignore temporary interaction (dragging sliders, etc.)
if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet
&& CachedPreviewData.IsValid()
&& Definition)
{
{
const FText PropertyName = PropertyChangedEvent.Property ? PropertyChangedEvent.Property->GetDisplayNameText() : FText::GetEmpty();
FScopedTransaction Transaction(FText::Format(LOCTEXT("OnPreviewValueChanged", "Set {0}"), PropertyName));
Definition->Modify();
if (const FSmartObjectDefinitionPreviewData* PreviewData = reinterpret_cast<const FSmartObjectDefinitionPreviewData*>(CachedPreviewData->GetStructMemory()))
{
Definition->PreviewData = *PreviewData;
}
}
UpdatePreviewActor();
}
});
if (USmartObjectDefinition* Definition = Cast<USmartObjectDefinition>(GetEditingObject()))
{
CachedPreviewData = MakeShared<TStructOnScope<FSmartObjectDefinitionPreviewData>>(Definition->PreviewData);
CachedPreviewData->SetPackage(Definition->GetPackage());
PreviewDetailsView->SetStructureData(CachedPreviewData);
}
TSharedRef<SDockTab> SpawnedTab = SNew(SDockTab)
.Label(LOCTEXT("PreviewSettingsTitle", "Preview Settings"))
[
PreviewDetailsView->GetWidget().ToSharedRef()
];
return SpawnedTab;
}
void FSmartObjectAssetToolkit::PostUndo(bool bSuccess)
{
UpdateItemList();
UpdateDetailsSelection();
UpdateCachedPreviewDataFromDefinition();
}
void FSmartObjectAssetToolkit::PostRedo(bool bSuccess)
{
UpdateItemList();
UpdateDetailsSelection();
UpdateCachedPreviewDataFromDefinition();
}
void FSmartObjectAssetToolkit::UpdateCachedPreviewDataFromDefinition()
{
const USmartObjectDefinition* Definition = Cast<USmartObjectDefinition>(GetEditingObject());
if (Definition
&& CachedPreviewData.IsValid())
{
if (FSmartObjectDefinitionPreviewData* PreviewData = reinterpret_cast<FSmartObjectDefinitionPreviewData*>(CachedPreviewData->GetStructMemory()))
{
*PreviewData = Definition->PreviewData;
}
UpdateDetailsSelection();
PreviewDetailsView->GetDetailsView()->ForceRefresh();
UpdatePreviewActor();
}
}
void FSmartObjectAssetToolkit::UpdatePreviewActor()
{
SmartObjectViewportClient->ResetPreviewActor();
const USmartObjectDefinition* Definition = CastChecked<USmartObjectDefinition>(GetEditingObject());
if (!Definition)
{
return;
}
if (!Definition->PreviewData.ObjectActorClass.IsNull())
{
SmartObjectViewportClient->SetPreviewActorClass(Definition->PreviewData.ObjectActorClass.LoadSynchronous());
}
else if (Definition->PreviewData.ObjectMeshPath.IsValid())
{
UStaticMesh* PreviewMesh = Cast<UStaticMesh>(Definition->PreviewData.ObjectMeshPath.TryLoad());
if (PreviewMesh)
{
SmartObjectViewportClient->SetPreviewMesh(PreviewMesh);
}
}
}
void FSmartObjectAssetToolkit::UpdateItemList()
{
const USmartObjectDefinition* Definition = CastChecked<USmartObjectDefinition>(GetEditingObject());
if (!Definition)
{
return;
}
// Flatten old items to make it easy to reuse them.
TArray<TSharedPtr<FSmartObjectOutlinerItem>> OldItemList;
UE::SmartObject::Editor::FlattenItemList(ItemList, OldItemList);
// Creates item by recycling old items. Recycling allows selections to persist across updates.
auto CreateItem = [&OldItemList](const FGuid ID, const ESmartObjectSlotItemType Type, TSharedPtr<FSmartObjectOutlinerItem> Parent) -> TSharedPtr<FSmartObjectOutlinerItem>
{
const int32 MatchIndex = OldItemList.IndexOfByPredicate([ID, Type](const TSharedPtr<FSmartObjectOutlinerItem>& Item)
{
return Item->ID == ID && Item->Type == Type;
});
if (MatchIndex != INDEX_NONE)
{
TSharedPtr<FSmartObjectOutlinerItem> Item = OldItemList[MatchIndex];
OldItemList.RemoveAtSwap(MatchIndex);
return Item;
}
TSharedPtr<FSmartObjectOutlinerItem> NewItem = MakeShared<FSmartObjectOutlinerItem>();
NewItem->ID = ID;
NewItem->Type = Type;
NewItem->Parent = Parent;
return NewItem;
};
// Object
{
TSharedPtr<FSmartObjectOutlinerItem> Item = CreateItem(FGuid(), ESmartObjectSlotItemType::Object, /*Parent*/nullptr);
ItemList.Add(Item);
// Definition data
for (const FSmartObjectDefinitionDataProxy& DataProxy : Definition->DefinitionData)
{
TSharedPtr<FSmartObjectOutlinerItem> DataOutlinerItem = CreateItem(DataProxy.ID, ESmartObjectSlotItemType::ObjectDefinitionData, Item);
Item->ChildItems.Add(DataOutlinerItem);
}
}
// Slots
for (const FSmartObjectSlotDefinition& Slot : Definition->GetSlots())
{
TSharedPtr<FSmartObjectOutlinerItem> SlotOutlinerItem = CreateItem(Slot.ID, ESmartObjectSlotItemType::Slot, /*Parent*/nullptr);
ItemList.Add(SlotOutlinerItem);
// Definition data
for (const FSmartObjectDefinitionDataProxy& DataProxy : Slot.DefinitionData)
{
TSharedPtr<FSmartObjectOutlinerItem> DataOutlinerItem = CreateItem(DataProxy.ID, ESmartObjectSlotItemType::SlotDefinitionData, SlotOutlinerItem);
SlotOutlinerItem->ChildItems.Add(DataOutlinerItem);
}
}
if (ItemTreeWidget)
{
ItemTreeWidget->RequestTreeRefresh();
}
}
void FSmartObjectAssetToolkit::OnClose()
{
FCoreUObjectDelegates::OnObjectPropertyChanged.RemoveAll(this);
FBaseAssetToolkit::OnClose();
}
void FSmartObjectAssetToolkit::AddReferencedObjects(FReferenceCollector& Collector)
{
}
void FSmartObjectAssetToolkit::OnPropertyChanged(UObject* ObjectBeingModified, FPropertyChangedEvent& PropertyChangedEvent)
{
if (ObjectBeingModified == nullptr || ObjectBeingModified != GetEditingObject())
{
return;
}
if (PropertyChangedEvent.GetMemberPropertyName() == GET_MEMBER_NAME_CHECKED(USmartObjectDefinition, Slots)
|| PropertyChangedEvent.GetMemberPropertyName() == GET_MEMBER_NAME_CHECKED(USmartObjectDefinition, DefinitionData))
{
GEditor->GetTimerManager()->SetTimerForNextTick([this]()
{
UpdateItemList();
});
}
}
#undef LOCTEXT_NAMESPACE