897 lines
23 KiB
C++
897 lines
23 KiB
C++
|
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||
|
|
|
||
|
|
#include "ItemEditorWindow.h"
|
||
|
|
|
||
|
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||
|
|
#include "AssetRegistry/IAssetRegistry.h"
|
||
|
|
#include "ContentBrowserModule.h"
|
||
|
|
#include "IContentBrowserSingleton.h"
|
||
|
|
#include "DetailsViewArgs.h"
|
||
|
|
#include "Editor.h"
|
||
|
|
#include "FileHelpers.h"
|
||
|
|
#include "IDetailsView.h"
|
||
|
|
#include "Modules/ModuleManager.h"
|
||
|
|
#include "PropertyEditorModule.h"
|
||
|
|
#include "Styling/CoreStyle.h"
|
||
|
|
#include "Styling/SlateColor.h"
|
||
|
|
#include "Subsystems/AssetEditorSubsystem.h"
|
||
|
|
#include "UObject/Class.h"
|
||
|
|
#include "UObject/UnrealType.h"
|
||
|
|
|
||
|
|
#include "Widgets/Input/SButton.h"
|
||
|
|
#include "Widgets/Input/SCheckBox.h"
|
||
|
|
#include "Widgets/Input/SSearchBox.h"
|
||
|
|
#include "Widgets/Layout/SBorder.h"
|
||
|
|
#include "Widgets/Layout/SSplitter.h"
|
||
|
|
#include "Widgets/Layout/SWrapBox.h"
|
||
|
|
#include "Widgets/SBoxPanel.h"
|
||
|
|
#include "Widgets/Text/STextBlock.h"
|
||
|
|
#include "Widgets/Views/STableRow.h"
|
||
|
|
|
||
|
|
#include "ProjectEleri/Items/DataAssets/ItemDataAsset.h"
|
||
|
|
|
||
|
|
#define LOCTEXT_NAMESPACE "ItemEditorWindow"
|
||
|
|
|
||
|
|
/*-----------------------------------------------------------------------------
|
||
|
|
Static helpers
|
||
|
|
-----------------------------------------------------------------------------*/
|
||
|
|
|
||
|
|
namespace
|
||
|
|
{
|
||
|
|
/** Best-effort conversion of the ItemCategory registry tag into an underlying Category enum value. */
|
||
|
|
int32 ParseCategoryFromRegistry(const FAssetData& InAsset)
|
||
|
|
{
|
||
|
|
const UEnum* Enum = StaticEnum<Category>();
|
||
|
|
if (!Enum)
|
||
|
|
{
|
||
|
|
return INDEX_NONE;
|
||
|
|
}
|
||
|
|
|
||
|
|
FString TagValue;
|
||
|
|
if (!InAsset.GetTagValue<FString>(TEXT("ItemCategory"), TagValue))
|
||
|
|
{
|
||
|
|
return INDEX_NONE;
|
||
|
|
}
|
||
|
|
|
||
|
|
TagValue.TrimStartAndEndInline();
|
||
|
|
if (TagValue.IsEmpty())
|
||
|
|
{
|
||
|
|
return INDEX_NONE;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Some exports store the underlying integer ("2"), most store the value name ("Plant").
|
||
|
|
if (TagValue.IsNumeric())
|
||
|
|
{
|
||
|
|
const int64 NumericValue = FCString::Atoi64(*TagValue);
|
||
|
|
return Enum->GetIndexByValue(NumericValue) != INDEX_NONE ? (int32)NumericValue : INDEX_NONE;
|
||
|
|
}
|
||
|
|
|
||
|
|
int64 ParsedValue = Enum->GetValueByNameString(TagValue);
|
||
|
|
if (ParsedValue != INDEX_NONE)
|
||
|
|
{
|
||
|
|
return (int32)ParsedValue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Defensive: strip a possible "Category::Plant" style prefix.
|
||
|
|
const int32 ScopeIndex = TagValue.Find(TEXT("::"));
|
||
|
|
if (ScopeIndex != INDEX_NONE)
|
||
|
|
{
|
||
|
|
ParsedValue = Enum->GetValueByNameString(TagValue.RightChop(ScopeIndex + 2));
|
||
|
|
if (ParsedValue != INDEX_NONE)
|
||
|
|
{
|
||
|
|
return (int32)ParsedValue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return INDEX_NONE;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Short display name of a Category value ("Plant"), or empty when not resolvable. */
|
||
|
|
FString GetCategoryShortName(int32 InCategoryValue)
|
||
|
|
{
|
||
|
|
const UEnum* Enum = StaticEnum<Category>();
|
||
|
|
if (!Enum || InCategoryValue < 0)
|
||
|
|
{
|
||
|
|
return FString();
|
||
|
|
}
|
||
|
|
return Enum->GetNameStringByValue((int64)InCategoryValue);
|
||
|
|
}
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
FString SItemEditorWindow::GetTaggedItemName(const FAssetData& InAsset)
|
||
|
|
{
|
||
|
|
FString ItemName;
|
||
|
|
if (InAsset.GetTagValue<FString>(TEXT("ItemName"), ItemName))
|
||
|
|
{
|
||
|
|
return ItemName;
|
||
|
|
}
|
||
|
|
return FString();
|
||
|
|
}
|
||
|
|
|
||
|
|
FString SItemEditorWindow::GetEntryDisplayName(const FItemListEntry& InEntry)
|
||
|
|
{
|
||
|
|
if (!InEntry.ItemName.IsEmpty())
|
||
|
|
{
|
||
|
|
return InEntry.ItemName;
|
||
|
|
}
|
||
|
|
// No ItemName tag on disk yet: fall back to the asset file name.
|
||
|
|
return InEntry.AssetData.AssetName.ToString();
|
||
|
|
}
|
||
|
|
|
||
|
|
FString SItemEditorWindow::BuildEntrySearchText(const FItemListEntry& InEntry)
|
||
|
|
{
|
||
|
|
FString Combined = FString::Printf(TEXT("%s %s %s %s"),
|
||
|
|
*GetEntryDisplayName(InEntry),
|
||
|
|
*GetCategoryShortName(InEntry.CategoryValue),
|
||
|
|
*InEntry.AssetData.AssetName.ToString(),
|
||
|
|
*InEntry.AssetData.PackageName.ToString());
|
||
|
|
return Combined.ToLower();
|
||
|
|
}
|
||
|
|
|
||
|
|
/*-----------------------------------------------------------------------------
|
||
|
|
Widget construction
|
||
|
|
-----------------------------------------------------------------------------*/
|
||
|
|
|
||
|
|
SItemEditorWindow::~SItemEditorWindow()
|
||
|
|
{
|
||
|
|
if (DetailsView.IsValid())
|
||
|
|
{
|
||
|
|
DetailsView->OnFinishedChangingProperties().RemoveAll(this);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void SItemEditorWindow::Construct(const FArguments& InArgs)
|
||
|
|
{
|
||
|
|
// --- Details view that edits the selected data asset object directly ---
|
||
|
|
FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>(TEXT("PropertyEditor"));
|
||
|
|
|
||
|
|
FDetailsViewArgs DetailsArgs;
|
||
|
|
DetailsArgs.bHideSelectionTip = true;
|
||
|
|
DetailsArgs.bLockable = false;
|
||
|
|
DetailsArgs.bAllowSearch = true;
|
||
|
|
DetailsArgs.bShowOptions = true;
|
||
|
|
DetailsArgs.bShowObjectLabel = false;
|
||
|
|
DetailsArgs.bShowModifiedPropertiesOption = true;
|
||
|
|
DetailsArgs.bAllowFavoriteSystem = true;
|
||
|
|
DetailsArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea;
|
||
|
|
|
||
|
|
DetailsView = PropertyEditorModule.CreateDetailView(DetailsArgs);
|
||
|
|
DetailsView->SetObject(nullptr);
|
||
|
|
DetailsView->OnFinishedChangingProperties().AddRaw(this, &SItemEditorWindow::OnAssetPropertyChanged);
|
||
|
|
|
||
|
|
InitCategoryFilter();
|
||
|
|
|
||
|
|
ChildSlot
|
||
|
|
[
|
||
|
|
SNew(SVerticalBox)
|
||
|
|
|
||
|
|
// ---- Search box + refresh button ------------------------------------------------
|
||
|
|
+ SVerticalBox::Slot()
|
||
|
|
.AutoHeight()
|
||
|
|
.Padding(4.f, 4.f, 4.f, 6.f)
|
||
|
|
[
|
||
|
|
SNew(SHorizontalBox)
|
||
|
|
|
||
|
|
+ SHorizontalBox::Slot()
|
||
|
|
.FillWidth(1.f)
|
||
|
|
.VAlign(VAlign_Center)
|
||
|
|
[
|
||
|
|
SAssignNew(SearchBox, SSearchBox)
|
||
|
|
.HintText(LOCTEXT("SearchHint", "Type an item name to find it..."))
|
||
|
|
.OnTextChanged(this, &SItemEditorWindow::OnSearchTextChanged)
|
||
|
|
.OnTextCommitted(this, &SItemEditorWindow::OnSearchTextCommitted)
|
||
|
|
]
|
||
|
|
|
||
|
|
+ SHorizontalBox::Slot()
|
||
|
|
.AutoWidth()
|
||
|
|
.Padding(6.f, 0.f, 0.f, 0.f)
|
||
|
|
[
|
||
|
|
SNew(SButton)
|
||
|
|
.Text(LOCTEXT("RefreshList", "Refresh"))
|
||
|
|
.ToolTipText(LOCTEXT("RefreshTip", "Re-scan the Asset Registry for every item data asset."))
|
||
|
|
.OnClicked(this, &SItemEditorWindow::OnRefreshClicked)
|
||
|
|
]
|
||
|
|
]
|
||
|
|
|
||
|
|
// ---- Category filter chips (multi select, empty selection = all) ----------------
|
||
|
|
+ SVerticalBox::Slot()
|
||
|
|
.AutoHeight()
|
||
|
|
.Padding(4.f, 0.f, 4.f, 6.f)
|
||
|
|
[
|
||
|
|
MakeCategoryFilterWidget()
|
||
|
|
]
|
||
|
|
|
||
|
|
// ---- List of items (left) + details view (right) -------------------------------
|
||
|
|
+ SVerticalBox::Slot()
|
||
|
|
.FillHeight(1.f)
|
||
|
|
.Padding(4.f, 0.f, 4.f, 0.f)
|
||
|
|
[
|
||
|
|
SNew(SSplitter)
|
||
|
|
.Orientation(Orient_Horizontal)
|
||
|
|
|
||
|
|
+ SSplitter::Slot()
|
||
|
|
.Value(0.32f)
|
||
|
|
.MinSize(240.f)
|
||
|
|
[
|
||
|
|
SNew(SBorder)
|
||
|
|
.BorderImage(FCoreStyle::Get().GetBrush(TEXT("Border")))
|
||
|
|
.Padding(4.f)
|
||
|
|
[
|
||
|
|
SNew(SVerticalBox)
|
||
|
|
|
||
|
|
+ SVerticalBox::Slot()
|
||
|
|
.AutoHeight()
|
||
|
|
.Padding(0.f, 2.f, 0.f, 4.f)
|
||
|
|
[
|
||
|
|
SNew(STextBlock)
|
||
|
|
.Text(LOCTEXT("ItemsLabel", "Items"))
|
||
|
|
.Font(FCoreStyle::GetDefaultFontStyle(TEXT("Bold"), 11))
|
||
|
|
]
|
||
|
|
|
||
|
|
+ SVerticalBox::Slot()
|
||
|
|
.FillHeight(1.f)
|
||
|
|
[
|
||
|
|
SAssignNew(ListView, SListView<FItemEntry>)
|
||
|
|
.ListItemsSource(&FilteredItems)
|
||
|
|
.SelectionMode(ESelectionMode::Single)
|
||
|
|
.OnGenerateRow(this, &SItemEditorWindow::OnGenerateRow)
|
||
|
|
.OnSelectionChanged(this, &SItemEditorWindow::OnItemSelectionChanged)
|
||
|
|
.OnMouseButtonDoubleClick(this, &SItemEditorWindow::OnItemDoubleClicked)
|
||
|
|
]
|
||
|
|
]
|
||
|
|
]
|
||
|
|
|
||
|
|
+ SSplitter::Slot()
|
||
|
|
.Value(0.68f)
|
||
|
|
.MinSize(360.f)
|
||
|
|
[
|
||
|
|
SNew(SBorder)
|
||
|
|
.BorderImage(FCoreStyle::Get().GetBrush(TEXT("Border")))
|
||
|
|
.Padding(4.f)
|
||
|
|
[
|
||
|
|
SNew(SVerticalBox)
|
||
|
|
|
||
|
|
+ SVerticalBox::Slot()
|
||
|
|
.AutoHeight()
|
||
|
|
.Padding(0.f, 2.f, 0.f, 6.f)
|
||
|
|
[
|
||
|
|
SNew(STextBlock)
|
||
|
|
.Text(this, &SItemEditorWindow::GetSelectionStatusText)
|
||
|
|
.Font(FCoreStyle::GetDefaultFontStyle(TEXT("Bold"), 11))
|
||
|
|
]
|
||
|
|
|
||
|
|
+ SVerticalBox::Slot()
|
||
|
|
.FillHeight(1.f)
|
||
|
|
[
|
||
|
|
DetailsView->AsShared()
|
||
|
|
]
|
||
|
|
]
|
||
|
|
]
|
||
|
|
]
|
||
|
|
|
||
|
|
// ---- Status bar with auto-save + save actions ----------------------------------
|
||
|
|
+ SVerticalBox::Slot()
|
||
|
|
.AutoHeight()
|
||
|
|
.Padding(4.f, 4.f, 4.f, 4.f)
|
||
|
|
[
|
||
|
|
SNew(SHorizontalBox)
|
||
|
|
|
||
|
|
+ SHorizontalBox::Slot()
|
||
|
|
.AutoWidth()
|
||
|
|
.VAlign(VAlign_Center)
|
||
|
|
.Padding(0.f, 0.f, 12.f, 0.f)
|
||
|
|
[
|
||
|
|
SNew(SCheckBox)
|
||
|
|
.ToolTipText(LOCTEXT("AutoSaveTip", "Save the data asset to disk automatically after every change."))
|
||
|
|
.IsChecked_Lambda([this]() -> ECheckBoxState
|
||
|
|
{
|
||
|
|
return bAutoSaveOnChange ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
|
||
|
|
})
|
||
|
|
.OnCheckStateChanged(this, &SItemEditorWindow::OnAutoSaveCheckChanged)
|
||
|
|
[
|
||
|
|
SNew(STextBlock)
|
||
|
|
.Text(LOCTEXT("AutoSaveLabel", "Auto-save on edit"))
|
||
|
|
]
|
||
|
|
]
|
||
|
|
|
||
|
|
+ SHorizontalBox::Slot()
|
||
|
|
.FillWidth(1.f)
|
||
|
|
.VAlign(VAlign_Center)
|
||
|
|
.Padding(0.f, 0.f, 12.f, 0.f)
|
||
|
|
[
|
||
|
|
SAssignNew(StatusText, STextBlock)
|
||
|
|
.Text(FText::GetEmpty())
|
||
|
|
.Font(FCoreStyle::GetDefaultFontStyle(TEXT("Regular"), 9))
|
||
|
|
]
|
||
|
|
|
||
|
|
+ SHorizontalBox::Slot()
|
||
|
|
.AutoWidth()
|
||
|
|
.Padding(4.f, 0.f, 0.f, 0.f)
|
||
|
|
[
|
||
|
|
SNew(SButton)
|
||
|
|
.Text(LOCTEXT("SaveAsset", "Save"))
|
||
|
|
.ToolTipText(LOCTEXT("SaveTip", "Write the selected data asset to disk."))
|
||
|
|
.IsEnabled_Lambda([this]() { return CurrentAsset.IsValid(); })
|
||
|
|
.OnClicked(this, &SItemEditorWindow::OnSaveClicked)
|
||
|
|
]
|
||
|
|
|
||
|
|
+ SHorizontalBox::Slot()
|
||
|
|
.AutoWidth()
|
||
|
|
.Padding(4.f, 0.f, 0.f, 0.f)
|
||
|
|
[
|
||
|
|
SNew(SButton)
|
||
|
|
.Text(LOCTEXT("OpenEditor", "Open Asset Editor"))
|
||
|
|
.ToolTipText(LOCTEXT("OpenEditorTip", "Open the selected asset in the regular data asset editor."))
|
||
|
|
.IsEnabled_Lambda([this]() { return CurrentAsset.IsValid(); })
|
||
|
|
.OnClicked(this, &SItemEditorWindow::OnOpenAssetEditorClicked)
|
||
|
|
]
|
||
|
|
|
||
|
|
+ SHorizontalBox::Slot()
|
||
|
|
.AutoWidth()
|
||
|
|
.Padding(4.f, 0.f, 0.f, 0.f)
|
||
|
|
[
|
||
|
|
SNew(SButton)
|
||
|
|
.Text(LOCTEXT("FindInCB", "Find in Content Browser"))
|
||
|
|
.ToolTipText(LOCTEXT("FindInCBTip", "Select the asset in the Content Browser."))
|
||
|
|
.IsEnabled_Lambda([this]() { return CurrentAsset.IsValid(); })
|
||
|
|
.OnClicked(this, &SItemEditorWindow::OnFindInContentBrowserClicked)
|
||
|
|
]
|
||
|
|
]
|
||
|
|
];
|
||
|
|
|
||
|
|
RefreshAssetList();
|
||
|
|
}
|
||
|
|
|
||
|
|
/*-----------------------------------------------------------------------------
|
||
|
|
Category filter
|
||
|
|
-----------------------------------------------------------------------------*/
|
||
|
|
|
||
|
|
void SItemEditorWindow::InitCategoryFilter()
|
||
|
|
{
|
||
|
|
CategoryFilterValues.Reset();
|
||
|
|
SelectedCategoryValues.Reset();
|
||
|
|
|
||
|
|
const UEnum* Enum = StaticEnum<Category>();
|
||
|
|
if (!Enum)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
for (int32 Index = 0; Index < Enum->NumEnums(); ++Index)
|
||
|
|
{
|
||
|
|
if (Enum->HasMetaData(TEXT("Hidden"), Index))
|
||
|
|
{
|
||
|
|
continue; // e.g. Category::MAX
|
||
|
|
}
|
||
|
|
|
||
|
|
const int64 Value = Enum->GetValueByIndex(Index);
|
||
|
|
if (Value >= 0)
|
||
|
|
{
|
||
|
|
CategoryFilterValues.Add((int32)Value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
TSharedRef<SWidget> SItemEditorWindow::MakeCategoryFilterWidget()
|
||
|
|
{
|
||
|
|
const UEnum* Enum = StaticEnum<Category>();
|
||
|
|
|
||
|
|
TSharedRef<SWrapBox> WrapBox = SNew(SWrapBox)
|
||
|
|
.UseAllottedSize(true)
|
||
|
|
.InnerSlotPadding(FVector2D(4.f, 3.f));
|
||
|
|
|
||
|
|
// "All" chip - clears every category selection.
|
||
|
|
WrapBox->AddSlot()
|
||
|
|
[
|
||
|
|
MakeCategoryCheckBox(INDEX_NONE, LOCTEXT("AllCategories", "All"))
|
||
|
|
];
|
||
|
|
|
||
|
|
for (const int32 CategoryValue : CategoryFilterValues)
|
||
|
|
{
|
||
|
|
FText Label;
|
||
|
|
if (Enum)
|
||
|
|
{
|
||
|
|
Label = Enum->GetDisplayNameTextByValue((int64)CategoryValue);
|
||
|
|
}
|
||
|
|
if (Label.IsEmpty())
|
||
|
|
{
|
||
|
|
Label = FText::FromString(FString::FromInt(CategoryValue));
|
||
|
|
}
|
||
|
|
|
||
|
|
WrapBox->AddSlot()
|
||
|
|
[
|
||
|
|
MakeCategoryCheckBox(CategoryValue, Label)
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
return SNew(SHorizontalBox)
|
||
|
|
|
||
|
|
+ SHorizontalBox::Slot()
|
||
|
|
.AutoWidth()
|
||
|
|
.VAlign(VAlign_Center)
|
||
|
|
.Padding(FMargin(0.f, 0.f, 10.f, 0.f))
|
||
|
|
[
|
||
|
|
SNew(STextBlock)
|
||
|
|
.Text(LOCTEXT("CategoryFilterLabel", "Category:"))
|
||
|
|
.Font(FCoreStyle::GetDefaultFontStyle(TEXT("Bold"), 10))
|
||
|
|
]
|
||
|
|
|
||
|
|
+ SHorizontalBox::Slot()
|
||
|
|
.FillWidth(1.f)
|
||
|
|
.VAlign(VAlign_Center)
|
||
|
|
[
|
||
|
|
WrapBox
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
TSharedRef<SWidget> SItemEditorWindow::MakeCategoryCheckBox(int32 InCategoryValue, const FText& InLabel)
|
||
|
|
{
|
||
|
|
return SNew(SCheckBox)
|
||
|
|
.Padding(FMargin(2.f, 1.f, 4.f, 1.f))
|
||
|
|
.IsChecked_Lambda([this, InCategoryValue]()
|
||
|
|
{
|
||
|
|
return IsCategoryChecked(InCategoryValue);
|
||
|
|
})
|
||
|
|
.OnCheckStateChanged_Lambda([this, InCategoryValue](ECheckBoxState InState)
|
||
|
|
{
|
||
|
|
OnCategoryToggled(InCategoryValue, InState);
|
||
|
|
})
|
||
|
|
[
|
||
|
|
SNew(STextBlock)
|
||
|
|
.Text(InLabel)
|
||
|
|
.Font(FCoreStyle::GetDefaultFontStyle(TEXT("Regular"), 10))
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
ECheckBoxState SItemEditorWindow::IsCategoryChecked(int32 InCategoryValue) const
|
||
|
|
{
|
||
|
|
if (InCategoryValue == INDEX_NONE)
|
||
|
|
{
|
||
|
|
// "All" is active when no specific category is selected.
|
||
|
|
return SelectedCategoryValues.Num() == 0 ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
|
||
|
|
}
|
||
|
|
return SelectedCategoryValues.Contains(InCategoryValue) ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
|
||
|
|
}
|
||
|
|
|
||
|
|
void SItemEditorWindow::OnCategoryToggled(int32 InCategoryValue, ECheckBoxState InState)
|
||
|
|
{
|
||
|
|
if (InCategoryValue == INDEX_NONE)
|
||
|
|
{
|
||
|
|
SelectedCategoryValues.Reset();
|
||
|
|
}
|
||
|
|
else if (InState == ECheckBoxState::Checked)
|
||
|
|
{
|
||
|
|
SelectedCategoryValues.Add(InCategoryValue);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
SelectedCategoryValues.Remove(InCategoryValue);
|
||
|
|
}
|
||
|
|
|
||
|
|
ApplyFilter();
|
||
|
|
SetStatusMessage(FString::Printf(TEXT("%d of %d items shown"), FilteredItems.Num(), ItemEntries.Num()));
|
||
|
|
}
|
||
|
|
|
||
|
|
/*-----------------------------------------------------------------------------
|
||
|
|
Item list / filtering
|
||
|
|
-----------------------------------------------------------------------------*/
|
||
|
|
|
||
|
|
void SItemEditorWindow::RefreshAssetList()
|
||
|
|
{
|
||
|
|
const UItemDataAsset* PreviouslySelected = GetCurrentAsset();
|
||
|
|
|
||
|
|
IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
|
||
|
|
|
||
|
|
TArray<FAssetData> Assets;
|
||
|
|
AssetRegistry.GetAssetsByClass(UItemDataAsset::StaticClass()->GetClassPathName(), Assets, /*bSearchSubClasses=*/true);
|
||
|
|
|
||
|
|
ItemEntries.Reset();
|
||
|
|
ItemEntries.Reserve(Assets.Num());
|
||
|
|
for (FAssetData& Asset : Assets)
|
||
|
|
{
|
||
|
|
TSharedPtr<FItemListEntry> Entry = MakeShared<FItemListEntry>();
|
||
|
|
Entry->AssetData = Asset;
|
||
|
|
|
||
|
|
// Prefer the live object so name/category are always exact; registry tags are the fallback.
|
||
|
|
UItemDataAsset* DataAsset = Cast<UItemDataAsset>(Asset.GetAsset());
|
||
|
|
if (DataAsset)
|
||
|
|
{
|
||
|
|
Entry->ItemName = DataAsset->ItemName.ToString();
|
||
|
|
Entry->CategoryValue = (int32)DataAsset->ItemCategory;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
Entry->ItemName = GetTaggedItemName(Asset);
|
||
|
|
Entry->CategoryValue = ParseCategoryFromRegistry(Asset);
|
||
|
|
}
|
||
|
|
|
||
|
|
Entry->SearchText = BuildEntrySearchText(*Entry);
|
||
|
|
ItemEntries.Add(Entry);
|
||
|
|
}
|
||
|
|
|
||
|
|
ApplyFilter();
|
||
|
|
|
||
|
|
// Restore the list highlight for the asset we were editing (details view stays untouched).
|
||
|
|
if (PreviouslySelected && ListView)
|
||
|
|
{
|
||
|
|
if (FItemEntry Entry = FindEntryForAsset(PreviouslySelected))
|
||
|
|
{
|
||
|
|
ListView->SetSelection(Entry, ESelectInfo::Direct);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
SetStatusMessage(FString::Printf(TEXT("%d item data assets found (%d shown)"), ItemEntries.Num(), FilteredItems.Num()));
|
||
|
|
}
|
||
|
|
|
||
|
|
void SItemEditorWindow::ApplyFilter()
|
||
|
|
{
|
||
|
|
FString SearchTerm = SearchFilterText.ToString();
|
||
|
|
SearchTerm.TrimStartAndEndInline();
|
||
|
|
const FString SearchLower = SearchTerm.ToLower();
|
||
|
|
|
||
|
|
FilteredItems.Reset();
|
||
|
|
const bool bFilteringByCategory = SelectedCategoryValues.Num() > 0;
|
||
|
|
|
||
|
|
for (const FItemEntry& Entry : ItemEntries)
|
||
|
|
{
|
||
|
|
// Name / asset search term.
|
||
|
|
if (!SearchLower.IsEmpty() && !Entry->SearchText.Contains(SearchLower))
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Category inclusion filter (empty selection shows every category).
|
||
|
|
if (bFilteringByCategory &&
|
||
|
|
(Entry->CategoryValue == INDEX_NONE || !SelectedCategoryValues.Contains(Entry->CategoryValue)))
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
FilteredItems.Add(Entry);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Case insensitive alphabetical sort by item name.
|
||
|
|
FilteredItems.Sort([](const FItemEntry& A, const FItemEntry& B)
|
||
|
|
{
|
||
|
|
const FString NameA = A->ItemName.ToLower();
|
||
|
|
const FString NameB = B->ItemName.ToLower();
|
||
|
|
if (NameA == NameB)
|
||
|
|
{
|
||
|
|
return A->AssetData.AssetName.LexicalLess(B->AssetData.AssetName);
|
||
|
|
}
|
||
|
|
return NameA < NameB;
|
||
|
|
});
|
||
|
|
|
||
|
|
if (ListView)
|
||
|
|
{
|
||
|
|
ListView->RequestListRefresh();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void SItemEditorWindow::SelectFirstVisibleItem()
|
||
|
|
{
|
||
|
|
if (FilteredItems.Num() == 0)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
FItemEntry FirstItem = FilteredItems[0];
|
||
|
|
ListView->SetSelection(FirstItem, ESelectInfo::OnNavigation);
|
||
|
|
ListView->RequestScrollIntoView(FirstItem);
|
||
|
|
}
|
||
|
|
|
||
|
|
SItemEditorWindow::FItemEntry SItemEditorWindow::FindEntryForAsset(const UItemDataAsset* InAsset)
|
||
|
|
{
|
||
|
|
if (!InAsset)
|
||
|
|
{
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
const FString AssetPath = InAsset->GetPathName();
|
||
|
|
for (const FItemEntry& Entry : ItemEntries)
|
||
|
|
{
|
||
|
|
if (Entry->AssetData.GetObjectPathString() == AssetPath)
|
||
|
|
{
|
||
|
|
return Entry;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
void SItemEditorWindow::UpdateEntryFromLoadedAsset(FItemEntry InEntry, const UItemDataAsset* InAsset)
|
||
|
|
{
|
||
|
|
if (!InEntry || !InAsset)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
InEntry->ItemName = InAsset->ItemName.ToString();
|
||
|
|
InEntry->CategoryValue = (int32)InAsset->ItemCategory;
|
||
|
|
InEntry->SearchText = BuildEntrySearchText(*InEntry);
|
||
|
|
}
|
||
|
|
|
||
|
|
/*-----------------------------------------------------------------------------
|
||
|
|
Item rows
|
||
|
|
-----------------------------------------------------------------------------*/
|
||
|
|
|
||
|
|
TSharedRef<ITableRow> SItemEditorWindow::OnGenerateRow(FItemEntry InItem, const TSharedRef<STableViewBase>& InOwnerTable)
|
||
|
|
{
|
||
|
|
check(InItem.IsValid());
|
||
|
|
|
||
|
|
const FString CategoryLabel = GetCategoryShortName(InItem->CategoryValue);
|
||
|
|
const FString AssetPath = CategoryLabel.IsEmpty()
|
||
|
|
? FString::Printf(TEXT("%s / %s"),
|
||
|
|
*InItem->AssetData.PackageName.ToString(),
|
||
|
|
*InItem->AssetData.AssetName.ToString())
|
||
|
|
: FString::Printf(TEXT("%s / %s (%s)"),
|
||
|
|
*InItem->AssetData.PackageName.ToString(),
|
||
|
|
*InItem->AssetData.AssetName.ToString(),
|
||
|
|
*CategoryLabel);
|
||
|
|
|
||
|
|
return SNew(STableRow<FItemEntry>, InOwnerTable)
|
||
|
|
.Padding(FMargin(4.f, 2.f, 4.f, 2.f))
|
||
|
|
[
|
||
|
|
SNew(SVerticalBox)
|
||
|
|
|
||
|
|
+ SVerticalBox::Slot()
|
||
|
|
.AutoHeight()
|
||
|
|
[
|
||
|
|
SNew(STextBlock)
|
||
|
|
.Text(FText::FromString(GetEntryDisplayName(*InItem)))
|
||
|
|
.Font(FCoreStyle::GetDefaultFontStyle(TEXT("Bold"), 10))
|
||
|
|
]
|
||
|
|
|
||
|
|
+ SVerticalBox::Slot()
|
||
|
|
.AutoHeight()
|
||
|
|
[
|
||
|
|
SNew(STextBlock)
|
||
|
|
.Text(FText::FromString(AssetPath))
|
||
|
|
.Font(FCoreStyle::GetDefaultFontStyle(TEXT("Regular"), 8))
|
||
|
|
.ColorAndOpacity(FSlateColor(FLinearColor(0.55f, 0.55f, 0.55f)))
|
||
|
|
]
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/*-----------------------------------------------------------------------------
|
||
|
|
Search & selection callbacks
|
||
|
|
-----------------------------------------------------------------------------*/
|
||
|
|
|
||
|
|
void SItemEditorWindow::OnSearchTextChanged(const FText& InText)
|
||
|
|
{
|
||
|
|
SearchFilterText = InText;
|
||
|
|
ApplyFilter();
|
||
|
|
|
||
|
|
if (FilteredItems.Num() == 0)
|
||
|
|
{
|
||
|
|
SetStatusMessage(TEXT("No items match that name."));
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
SetStatusMessage(FString::Printf(TEXT("%d of %d items shown"), FilteredItems.Num(), ItemEntries.Num()));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void SItemEditorWindow::OnSearchTextCommitted(const FText& InText, ETextCommit::Type InCommitType)
|
||
|
|
{
|
||
|
|
if (InCommitType == ETextCommit::OnEnter)
|
||
|
|
{
|
||
|
|
SelectFirstVisibleItem();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void SItemEditorWindow::OnItemSelectionChanged(FItemEntry InItem, ESelectInfo::Type InSelectInfo)
|
||
|
|
{
|
||
|
|
// Programmatic selections (e.g. restoring the highlight after a refresh) are already handled.
|
||
|
|
if (InSelectInfo == ESelectInfo::Direct)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!InItem)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
UItemDataAsset* Asset = LoadAssetForEntry(InItem);
|
||
|
|
if (!Asset)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
CurrentAsset = Asset;
|
||
|
|
DetailsView->SetObject(Asset, /*bForceRefresh=*/true);
|
||
|
|
SetStatusMessage(FString::Printf(TEXT("Editing %s"), *Asset->GetPathName()));
|
||
|
|
}
|
||
|
|
|
||
|
|
void SItemEditorWindow::OnItemDoubleClicked(FItemEntry InItem)
|
||
|
|
{
|
||
|
|
if (!InItem)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
UItemDataAsset* Asset = LoadAssetForEntry(InItem);
|
||
|
|
if (!Asset)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
CurrentAsset = Asset;
|
||
|
|
DetailsView->SetObject(Asset, /*bForceRefresh=*/true);
|
||
|
|
OpenInAssetEditor(Asset);
|
||
|
|
}
|
||
|
|
|
||
|
|
void SItemEditorWindow::OnAssetPropertyChanged(const FPropertyChangedEvent& InEvent)
|
||
|
|
{
|
||
|
|
if (UItemDataAsset* Asset = GetCurrentAsset())
|
||
|
|
{
|
||
|
|
// Keep the list label in sync (e.g. when ItemName is edited).
|
||
|
|
if (FItemEntry Entry = FindEntryForAsset(Asset))
|
||
|
|
{
|
||
|
|
UpdateEntryFromLoadedAsset(Entry, Asset);
|
||
|
|
if (ListView)
|
||
|
|
{
|
||
|
|
ListView->RequestListRefresh();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (bAutoSaveOnChange)
|
||
|
|
{
|
||
|
|
SaveCurrentAsset();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/*-----------------------------------------------------------------------------
|
||
|
|
Asset loading / accessors
|
||
|
|
-----------------------------------------------------------------------------*/
|
||
|
|
|
||
|
|
UItemDataAsset* SItemEditorWindow::GetCurrentAsset() const
|
||
|
|
{
|
||
|
|
return CurrentAsset.Get();
|
||
|
|
}
|
||
|
|
|
||
|
|
UItemDataAsset* SItemEditorWindow::LoadAssetForEntry(FItemEntry InItem)
|
||
|
|
{
|
||
|
|
if (!InItem)
|
||
|
|
{
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
UObject* LoadedObject = InItem->AssetData.GetAsset();
|
||
|
|
if (!LoadedObject)
|
||
|
|
{
|
||
|
|
SetStatusMessage(FString::Printf(TEXT("Failed to load %s."), *InItem->AssetData.GetObjectPathString()), /*bIsError=*/true);
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
UItemDataAsset* Asset = Cast<UItemDataAsset>(LoadedObject);
|
||
|
|
if (!Asset)
|
||
|
|
{
|
||
|
|
SetStatusMessage(FString::Printf(TEXT("%s is not an ItemDataAsset."), *InItem->AssetData.GetObjectPathString()), /*bIsError=*/true);
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
return Asset;
|
||
|
|
}
|
||
|
|
|
||
|
|
/*-----------------------------------------------------------------------------
|
||
|
|
Actions
|
||
|
|
-----------------------------------------------------------------------------*/
|
||
|
|
|
||
|
|
FReply SItemEditorWindow::OnRefreshClicked()
|
||
|
|
{
|
||
|
|
RefreshAssetList();
|
||
|
|
return FReply::Handled();
|
||
|
|
}
|
||
|
|
|
||
|
|
FReply SItemEditorWindow::OnSaveClicked()
|
||
|
|
{
|
||
|
|
SaveCurrentAsset();
|
||
|
|
return FReply::Handled();
|
||
|
|
}
|
||
|
|
|
||
|
|
FReply SItemEditorWindow::OnOpenAssetEditorClicked()
|
||
|
|
{
|
||
|
|
if (UItemDataAsset* Asset = GetCurrentAsset())
|
||
|
|
{
|
||
|
|
OpenInAssetEditor(Asset);
|
||
|
|
}
|
||
|
|
return FReply::Handled();
|
||
|
|
}
|
||
|
|
|
||
|
|
FReply SItemEditorWindow::OnFindInContentBrowserClicked()
|
||
|
|
{
|
||
|
|
if (UItemDataAsset* Asset = GetCurrentAsset())
|
||
|
|
{
|
||
|
|
ShowInContentBrowser(Asset);
|
||
|
|
}
|
||
|
|
return FReply::Handled();
|
||
|
|
}
|
||
|
|
|
||
|
|
void SItemEditorWindow::OnAutoSaveCheckChanged(ECheckBoxState InState)
|
||
|
|
{
|
||
|
|
bAutoSaveOnChange = (InState == ECheckBoxState::Checked);
|
||
|
|
SetStatusMessage(bAutoSaveOnChange ? TEXT("Auto-save enabled") : TEXT("Auto-save disabled"));
|
||
|
|
}
|
||
|
|
|
||
|
|
void SItemEditorWindow::SaveCurrentAsset()
|
||
|
|
{
|
||
|
|
UItemDataAsset* Asset = GetCurrentAsset();
|
||
|
|
if (!Asset)
|
||
|
|
{
|
||
|
|
SetStatusMessage(TEXT("No item selected."), /*bIsError=*/true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
UPackage* Package = Asset->GetPackage();
|
||
|
|
if (!Package)
|
||
|
|
{
|
||
|
|
SetStatusMessage(TEXT("Selected item has no package."), /*bIsError=*/true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
Package->SetDirtyFlag(true);
|
||
|
|
if (UEditorLoadingAndSavingUtils::SavePackages(TArray<UPackage*>{Package}, /*bOnlyDirty=*/true))
|
||
|
|
{
|
||
|
|
if (FItemEntry Entry = FindEntryForAsset(Asset))
|
||
|
|
{
|
||
|
|
UpdateEntryFromLoadedAsset(Entry, Asset);
|
||
|
|
if (ListView)
|
||
|
|
{
|
||
|
|
ListView->RequestListRefresh();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
SetStatusMessage(FString::Printf(TEXT("Saved %s"), *Asset->GetName()));
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
SetStatusMessage(FString::Printf(TEXT("Failed to save %s."), *Asset->GetName()), /*bIsError=*/true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void SItemEditorWindow::OpenInAssetEditor(UItemDataAsset* InAsset)
|
||
|
|
{
|
||
|
|
if (!InAsset)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>())
|
||
|
|
{
|
||
|
|
AssetEditorSubsystem->OpenEditorForAsset(InAsset);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void SItemEditorWindow::ShowInContentBrowser(UItemDataAsset* InAsset)
|
||
|
|
{
|
||
|
|
if (!InAsset)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser"));
|
||
|
|
ContentBrowserModule.Get().SyncBrowserToAssets(TArray<UObject*>{InAsset});
|
||
|
|
}
|
||
|
|
|
||
|
|
void SItemEditorWindow::SetStatusMessage(const FString& InMessage, bool bIsError)
|
||
|
|
{
|
||
|
|
if (!StatusText)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const FLinearColor TextColor = bIsError ? FLinearColor(1.f, 0.35f, 0.35f) : FLinearColor(0.7f, 0.7f, 0.7f);
|
||
|
|
StatusText->SetColorAndOpacity(FSlateColor(TextColor));
|
||
|
|
StatusText->SetText(FText::FromString(InMessage));
|
||
|
|
}
|
||
|
|
|
||
|
|
FText SItemEditorWindow::GetSelectionStatusText() const
|
||
|
|
{
|
||
|
|
if (UItemDataAsset* Asset = GetCurrentAsset())
|
||
|
|
{
|
||
|
|
return FText::FromString(Asset->GetPathName());
|
||
|
|
}
|
||
|
|
return LOCTEXT("NoSelection", "Select an item to edit it");
|
||
|
|
}
|
||
|
|
|
||
|
|
#undef LOCTEXT_NAMESPACE
|
||
|
|
|