// 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(); if (!Enum) { return INDEX_NONE; } FString TagValue; if (!InAsset.GetTagValue(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(); if (!Enum || InCategoryValue < 0) { return FString(); } return Enum->GetNameStringByValue((int64)InCategoryValue); } } // namespace FString SItemEditorWindow::GetTaggedItemName(const FAssetData& InAsset) { FString ItemName; if (InAsset.GetTagValue(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(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) .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(); 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 SItemEditorWindow::MakeCategoryFilterWidget() { const UEnum* Enum = StaticEnum(); TSharedRef 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 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(TEXT("AssetRegistry")).Get(); TArray Assets; AssetRegistry.GetAssetsByClass(UItemDataAsset::StaticClass()->GetClassPathName(), Assets, /*bSearchSubClasses=*/true); ItemEntries.Reset(); ItemEntries.Reserve(Assets.Num()); for (FAssetData& Asset : Assets) { TSharedPtr Entry = MakeShared(); Entry->AssetData = Asset; // Prefer the live object so name/category are always exact; registry tags are the fallback. UItemDataAsset* DataAsset = Cast(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 SItemEditorWindow::OnGenerateRow(FItemEntry InItem, const TSharedRef& 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, 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(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{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()) { AssetEditorSubsystem->OpenEditorForAsset(InAsset); } } void SItemEditorWindow::ShowInContentBrowser(UItemDataAsset* InAsset) { if (!InAsset) { return; } FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked(TEXT("ContentBrowser")); ContentBrowserModule.Get().SyncBrowserToAssets(TArray{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