// Copyright Epic Games, Inc. All Rights Reserved. #include "DataTableEditor.h" #include "AssetRegistry/AssetData.h" #include "Containers/Map.h" #include "CoreGlobals.h" #include "DataTableEditorModule.h" #include "DataTableUtils.h" #include "DetailsViewArgs.h" #include "Dom/JsonObject.h" #include "Editor.h" #include "Editor/EditorEngine.h" #include "Engine/DataTable.h" #include "StructUtils/UserDefinedStruct.h" #include "Fonts/FontMeasure.h" #include "Framework/Application/SlateApplication.h" #include "Framework/Commands/GenericCommands.h" #include "Framework/Commands/UIAction.h" #include "Framework/Commands/UICommandList.h" #include "Framework/Docking/TabManager.h" #include "Framework/Layout/Overscroll.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Framework/MultiBox/MultiBoxExtender.h" #include "Framework/Notifications/NotificationManager.h" #include "Framework/Text/TextLayout.h" #include "Framework/Views/ITypedTableView.h" #include "HAL/PlatformApplicationMisc.h" #include "IDetailsView.h" #include "IDocumentation.h" #include "Internationalization/Internationalization.h" #include "Layout/BasicLayoutWidgetSlot.h" #include "Layout/Margin.h" #include "Layout/Visibility.h" #include "Math/ColorList.h" #include "Math/UnrealMathSSE.h" #include "Math/Vector2D.h" #include "Misc/AssertionMacros.h" #include "Misc/Attribute.h" #include "Misc/FeedbackContext.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "Modules/ModuleManager.h" #include "Policies/PrettyJsonPrintPolicy.h" #include "PropertyEditorModule.h" #include "Rendering/SlateRenderer.h" #include "SDataTableListViewRow.h" #include "SRowEditor.h" #include "ScopedTransaction.h" #include "Serialization/JsonReader.h" #include "Serialization/JsonSerializer.h" #include "Serialization/JsonWriter.h" #include "SlotBase.h" #include "SourceCodeNavigation.h" #include "Styling/AppStyle.h" #include "Styling/SlateTypes.h" #include "Subsystems/AssetEditorSubsystem.h" #include "Templates/Casts.h" #include "Templates/TypeHash.h" #include "Textures/SlateIcon.h" #include "Toolkits/AssetEditorToolkit.h" #include "UObject/Class.h" #include "UObject/ObjectMacros.h" #include "UObject/ObjectPtr.h" #include "UObject/PropertyPortFlags.h" #include "UObject/TopLevelAssetPath.h" #include "UObject/UObjectBaseUtility.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/Docking/SDockTab.h" #include "Widgets/Images/SImage.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SHyperlink.h" #include "Widgets/Input/SSearchBox.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SScrollBar.h" #include "Widgets/Layout/SScrollBox.h" #include "Widgets/Notifications/SNotificationList.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SNullWidget.h" #include "Widgets/SToolTip.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Views/SListView.h" class ITableRow; class STableViewBase; class SWidget; #define LOCTEXT_NAMESPACE "DataTableEditor" const FName FDataTableEditor::DataTableTabId("DataTableEditor_DataTable"); const FName FDataTableEditor::DataTableDetailsTabId("DataTableEditor_DataTableDetails"); const FName FDataTableEditor::RowEditorTabId("DataTableEditor_RowEditor"); const FName FDataTableEditor::RowNameColumnId("RowName"); const FName FDataTableEditor::RowNumberColumnId("RowNumber"); const FName FDataTableEditor::RowDragDropColumnId("RowDragDrop"); class SDataTableModeSeparator : public SBorder { public: SLATE_BEGIN_ARGS(SDataTableModeSeparator) {} SLATE_END_ARGS() void Construct(const FArguments& InArg) { SBorder::Construct( SBorder::FArguments() .BorderImage(FAppStyle::GetBrush("BlueprintEditor.PipelineSeparator")) .Padding(0.0f) ); } // SWidget interface virtual FVector2D ComputeDesiredSize(float) const override { const float Height = 20.0f; const float Thickness = 16.0f; return FVector2D(Thickness, Height); } // End of SWidget interface }; void FDataTableEditor::RegisterTabSpawners(const TSharedRef& InTabManager) { WorkspaceMenuCategory = InTabManager->AddLocalWorkspaceMenuCategory(LOCTEXT("WorkspaceMenu_Data Table Editor", "Data Table Editor")); FAssetEditorToolkit::RegisterTabSpawners(InTabManager); CreateAndRegisterDataTableTab(InTabManager); CreateAndRegisterDataTableDetailsTab(InTabManager); CreateAndRegisterRowEditorTab(InTabManager); } void FDataTableEditor::UnregisterTabSpawners(const TSharedRef& InTabManager) { FAssetEditorToolkit::UnregisterTabSpawners(InTabManager); InTabManager->UnregisterTabSpawner(DataTableTabId); InTabManager->UnregisterTabSpawner(DataTableDetailsTabId); InTabManager->UnregisterTabSpawner(RowEditorTabId); DataTableTabWidget.Reset(); RowEditorTabWidget.Reset(); } void FDataTableEditor::CreateAndRegisterDataTableTab(const TSharedRef& InTabManager) { DataTableTabWidget = CreateContentBox(); InTabManager->RegisterTabSpawner(DataTableTabId, FOnSpawnTab::CreateSP(this, &FDataTableEditor::SpawnTab_DataTable)) .SetDisplayName(LOCTEXT("DataTableTab", "Data Table")) .SetGroup(WorkspaceMenuCategory.ToSharedRef()); } void FDataTableEditor::CreateAndRegisterDataTableDetailsTab(const TSharedRef& InTabManager) { FPropertyEditorModule & EditModule = FModuleManager::Get().GetModuleChecked("PropertyEditor"); FDetailsViewArgs DetailsViewArgs; DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea; DetailsViewArgs.bHideSelectionTip = true; PropertyView = EditModule.CreateDetailView(DetailsViewArgs); InTabManager->RegisterTabSpawner(DataTableDetailsTabId, FOnSpawnTab::CreateSP(this, &FDataTableEditor::SpawnTab_DataTableDetails)) .SetDisplayName(LOCTEXT("DataTableDetailsTab", "Data Table Details")) .SetGroup(WorkspaceMenuCategory.ToSharedRef()); } void FDataTableEditor::CreateAndRegisterRowEditorTab(const TSharedRef& InTabManager) { RowEditorTabWidget = CreateRowEditorBox(); InTabManager->RegisterTabSpawner(RowEditorTabId, FOnSpawnTab::CreateSP(this, &FDataTableEditor::SpawnTab_RowEditor)) .SetDisplayName(LOCTEXT("RowEditorTab", "Row Editor")) .SetGroup(WorkspaceMenuCategory.ToSharedRef()); } FDataTableEditor::FDataTableEditor() : RowNameColumnWidth(0) , RowNumberColumnWidth(0) , HighlightedVisibleRowIndex(INDEX_NONE) , SortMode(EColumnSortMode::Ascending) { } FDataTableEditor::~FDataTableEditor() { GEditor->UnregisterForUndo(this); UDataTable* Table = GetEditableDataTable(); if (Table) { SaveLayoutData(); } } void FDataTableEditor::PostUndo(bool bSuccess) { HandleUndoRedo(); } void FDataTableEditor::PostRedo(bool bSuccess) { HandleUndoRedo(); } void FDataTableEditor::HandleUndoRedo() { const UDataTable* Table = GetDataTable(); if (Table) { HandlePostChange(); CallbackOnDataTableUndoRedo.ExecuteIfBound(); } } void FDataTableEditor::PreChange(const class UUserDefinedStruct* Struct, FStructureEditorUtils::EStructureEditorChangeInfo Info) { } void FDataTableEditor::PostChange(const class UUserDefinedStruct* Struct, FStructureEditorUtils::EStructureEditorChangeInfo Info) { const UDataTable* Table = GetDataTable(); if (Struct && Table && (Table->GetRowStruct() == Struct)) { HandlePostChange(); } } void FDataTableEditor::SelectionChange(const UDataTable* Changed, FName RowName) { const UDataTable* Table = GetDataTable(); if (Changed == Table) { const bool bSelectionChanged = HighlightedRowName != RowName; SetHighlightedRow(RowName); if (bSelectionChanged) { CallbackOnRowHighlighted.ExecuteIfBound(HighlightedRowName); } } } void FDataTableEditor::PreChange(const UDataTable* Changed, FDataTableEditorUtils::EDataTableChangeInfo Info) { } void FDataTableEditor::PostChange(const UDataTable* Changed, FDataTableEditorUtils::EDataTableChangeInfo Info) { UDataTable* Table = GetEditableDataTable(); if (Changed == Table) { // Don't need to notify the DataTable about changes, that's handled before this HandlePostChange(); } } const UDataTable* FDataTableEditor::GetDataTable() const { return Cast(GetEditingObject()); } void FDataTableEditor::HandlePostChange() { // We need to cache and restore the selection here as RefreshCachedDataTable will re-create the list view items const FName CachedSelection = HighlightedRowName; HighlightedRowName = NAME_None; RefreshCachedDataTable(CachedSelection, true/*bUpdateEvenIfValid*/); } void FDataTableEditor::InitDataTableEditor( const EToolkitMode::Type Mode, const TSharedPtr< class IToolkitHost >& InitToolkitHost, UDataTable* Table ) { TSharedRef StandaloneDefaultLayout = FTabManager::NewLayout( "Standalone_DataTableEditor_Layout_v6" ) ->AddArea ( FTabManager::NewPrimaryArea()->SetOrientation(Orient_Vertical) ->Split ( FTabManager::NewStack() ->AddTab(DataTableTabId, ETabState::OpenedTab) ->AddTab(DataTableDetailsTabId, ETabState::OpenedTab) ->SetForegroundTab(DataTableTabId) ) ->Split ( FTabManager::NewStack() ->AddTab(RowEditorTabId, ETabState::OpenedTab) ) ); const bool bCreateDefaultStandaloneMenu = true; const bool bCreateDefaultToolbar = true; FAssetEditorToolkit::InitAssetEditor( Mode, InitToolkitHost, FDataTableEditorModule::DataTableEditorAppIdentifier, StandaloneDefaultLayout, bCreateDefaultStandaloneMenu, bCreateDefaultToolbar, Table ); FDataTableEditorModule& DataTableEditorModule = FModuleManager::LoadModuleChecked( "DataTableEditor" ); AddMenuExtender(DataTableEditorModule.GetMenuExtensibilityManager()->GetAllExtenders(GetToolkitCommands(), GetEditingObjects())); TSharedPtr ToolbarExtender = DataTableEditorModule.GetToolBarExtensibilityManager()->GetAllExtenders(GetToolkitCommands(), GetEditingObjects()); ExtendToolbar(ToolbarExtender); AddToolbarExtender(ToolbarExtender); RegenerateMenusAndToolbars(); // Support undo/redo GEditor->RegisterForUndo(this); // @todo toolkit world centric editing /*// Setup our tool's layout if( IsWorldCentricAssetEditor() ) { const FString TabInitializationPayload(TEXT("")); // NOTE: Payload not currently used for table properties SpawnToolkitTab( DataTableTabId, TabInitializationPayload, EToolkitTabSpot::Details ); }*/ // asset editor commands here ToolkitCommands->MapAction(FGenericCommands::Get().Copy, FExecuteAction::CreateSP(this, &FDataTableEditor::CopySelectedRow), FCanExecuteAction::CreateSP(this, &FDataTableEditor::CanEditTable)); ToolkitCommands->MapAction(FGenericCommands::Get().Paste, FExecuteAction::CreateSP(this, &FDataTableEditor::PasteOnSelectedRow), FCanExecuteAction::CreateSP(this, &FDataTableEditor::CanEditTable)); ToolkitCommands->MapAction(FGenericCommands::Get().Duplicate, FExecuteAction::CreateSP(this, &FDataTableEditor::DuplicateSelectedRow), FCanExecuteAction::CreateSP(this, &FDataTableEditor::CanEditTable)); ToolkitCommands->MapAction(FGenericCommands::Get().Rename, FExecuteAction::CreateSP(this, &FDataTableEditor::RenameSelectedRowCommand), FCanExecuteAction::CreateSP(this, &FDataTableEditor::CanEditTable)); ToolkitCommands->MapAction(FGenericCommands::Get().Delete, FExecuteAction::CreateSP(this, &FDataTableEditor::DeleteSelectedRow), FCanExecuteAction::CreateSP(this, &FDataTableEditor::CanEditTable)); } bool FDataTableEditor::CanEditRows() const { return true; } FName FDataTableEditor::GetToolkitFName() const { return FName("DataTableEditor"); } FString FDataTableEditor::GetDocumentationLink() const { return FString(TEXT("Gameplay/DataDriven")); } void FDataTableEditor::OnAddClicked() { UDataTable* Table = GetEditableDataTable(); if (Table) { FName NewName = DataTableUtils::MakeValidName(TEXT("NewRow")); while (Table->GetRowMap().Contains(NewName)) { NewName.SetNumber(NewName.GetNumber() + 1); } FDataTableEditorUtils::AddRow(Table, NewName); FDataTableEditorUtils::SelectRow(Table, NewName); SetDefaultSort(); } } void FDataTableEditor::OnRemoveClicked() { DeleteSelectedRow(); } FReply FDataTableEditor::OnMoveRowClicked(FDataTableEditorUtils::ERowMoveDirection MoveDirection) { UDataTable* Table = GetEditableDataTable(); if (Table) { FDataTableEditorUtils::MoveRow(Table, HighlightedRowName, MoveDirection); } return FReply::Handled(); } FReply FDataTableEditor::OnMoveToExtentClicked(FDataTableEditorUtils::ERowMoveDirection MoveDirection) { UDataTable* Table = GetEditableDataTable(); if (Table) { // We move by the row map size, as FDataTableEditorUtils::MoveRow will automatically clamp this as appropriate FDataTableEditorUtils::MoveRow(Table, HighlightedRowName, MoveDirection, Table->GetRowMap().Num()); FDataTableEditorUtils::SelectRow(Table, HighlightedRowName); SetDefaultSort(); } return FReply::Handled(); } void FDataTableEditor::OnCopyClicked() { UDataTable* Table = GetEditableDataTable(); if (Table) { CopySelectedRow(); } } void FDataTableEditor::OnPasteClicked() { UDataTable* Table = GetEditableDataTable(); if (Table) { PasteOnSelectedRow(); } } void FDataTableEditor::OnDuplicateClicked() { UDataTable* Table = GetEditableDataTable(); if (Table) { DuplicateSelectedRow(); } } bool FDataTableEditor::CanEditTable() const { return HighlightedRowName != NAME_None; } void FDataTableEditor::SetDefaultSort() { SortMode = EColumnSortMode::Ascending; SortByColumn = FDataTableEditor::RowNumberColumnId; } EColumnSortMode::Type FDataTableEditor::GetColumnSortMode(const FName ColumnId) const { if (SortByColumn != ColumnId) { return EColumnSortMode::None; } return SortMode; } void FDataTableEditor::OnColumnSortModeChanged(const EColumnSortPriority::Type SortPriority, const FName& ColumnId, const EColumnSortMode::Type InSortMode) { int32 ColumnIndex; SortMode = InSortMode; SortByColumn = ColumnId; for (ColumnIndex = 0; ColumnIndex < AvailableColumns.Num(); ++ColumnIndex) { if (AvailableColumns[ColumnIndex]->ColumnId == ColumnId) { break; } } if (AvailableColumns.IsValidIndex(ColumnIndex)) { if (InSortMode == EColumnSortMode::Ascending) { VisibleRows.Sort([ColumnIndex](const FDataTableEditorRowListViewDataPtr& first, const FDataTableEditorRowListViewDataPtr& second) { int32 Result = (first->CellData[ColumnIndex].ToString()).Compare(second->CellData[ColumnIndex].ToString()); if (!Result) { return first->RowNum < second->RowNum; } return Result < 0; }); } else if (InSortMode == EColumnSortMode::Descending) { VisibleRows.Sort([ColumnIndex](const FDataTableEditorRowListViewDataPtr& first, const FDataTableEditorRowListViewDataPtr& second) { int32 Result = (first->CellData[ColumnIndex].ToString()).Compare(second->CellData[ColumnIndex].ToString()); if (!Result) { return first->RowNum > second->RowNum; } return Result > 0; }); } } CellsListView->RequestListRefresh(); } void FDataTableEditor::OnColumnNumberSortModeChanged(const EColumnSortPriority::Type SortPriority, const FName& ColumnId, const EColumnSortMode::Type InSortMode) { SortMode = InSortMode; SortByColumn = ColumnId; if (InSortMode == EColumnSortMode::Ascending) { VisibleRows.Sort([](const FDataTableEditorRowListViewDataPtr& first, const FDataTableEditorRowListViewDataPtr& second) { return first->RowNum < second->RowNum; }); } else if (InSortMode == EColumnSortMode::Descending) { VisibleRows.Sort([](const FDataTableEditorRowListViewDataPtr& first, const FDataTableEditorRowListViewDataPtr& second) { return first->RowNum > second->RowNum; }); } CellsListView->RequestListRefresh(); } void FDataTableEditor::OnColumnNameSortModeChanged(const EColumnSortPriority::Type SortPriority, const FName& ColumnId, const EColumnSortMode::Type InSortMode) { SortMode = InSortMode; SortByColumn = ColumnId; if (InSortMode == EColumnSortMode::Ascending) { VisibleRows.Sort([](const FDataTableEditorRowListViewDataPtr& first, const FDataTableEditorRowListViewDataPtr& second) { return (first->DisplayName).ToString() < (second->DisplayName).ToString(); }); } else if (InSortMode == EColumnSortMode::Descending) { VisibleRows.Sort([](const FDataTableEditorRowListViewDataPtr& first, const FDataTableEditorRowListViewDataPtr& second) { return (first->DisplayName).ToString() > (second->DisplayName).ToString(); }); } CellsListView->RequestListRefresh(); } void FDataTableEditor::OnEditDataTableStructClicked() { const UDataTable* DataTable = GetDataTable(); if (DataTable) { const UScriptStruct* ScriptStruct = DataTable->GetRowStruct(); if (ScriptStruct) { GEditor->GetEditorSubsystem()->OpenEditorForAsset(ScriptStruct->GetPathName()); if (FSourceCodeNavigation::CanNavigateToStruct(ScriptStruct)) { FSourceCodeNavigation::NavigateToStruct(ScriptStruct); } } } } void FDataTableEditor::ExtendToolbar(TSharedPtr Extender) { Extender->AddToolBarExtension( "Asset", EExtensionHook::After, GetToolkitCommands(), FToolBarExtensionDelegate::CreateSP(this, &FDataTableEditor::FillToolbar) ); } void FDataTableEditor::FillToolbar(FToolBarBuilder& ToolbarBuilder) { ToolbarBuilder.BeginSection("DataTableCommands"); { ToolbarBuilder.AddToolBarButton( FUIAction( FExecuteAction::CreateSP(this, &FDataTableEditor::Reimport_Execute), FCanExecuteAction::CreateSP(this, &FDataTableEditor::CanReimport)), NAME_None, LOCTEXT("ReimportText", "Reimport"), LOCTEXT("ReimportTooltip", "Reimport this DataTable"), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Reimport")); ToolbarBuilder.AddSeparator(); ToolbarBuilder.AddToolBarButton( FUIAction(FExecuteAction::CreateSP(this, &FDataTableEditor::OnAddClicked)), NAME_None, LOCTEXT("AddIconText", "Add"), LOCTEXT("AddRowToolTip", "Add a new row to the Data Table"), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Plus")); ToolbarBuilder.AddToolBarButton( FUIAction( FExecuteAction::CreateSP(this, &FDataTableEditor::OnCopyClicked), FCanExecuteAction::CreateSP(this, &FDataTableEditor::CanEditTable)), NAME_None, LOCTEXT("CopyIconText", "Copy"), LOCTEXT("CopyToolTip", "Copy the currently selected row"), FSlateIcon(FAppStyle::GetAppStyleSetName(), "GenericCommands.Copy")); ToolbarBuilder.AddToolBarButton( FUIAction( FExecuteAction::CreateSP(this, &FDataTableEditor::OnPasteClicked), FCanExecuteAction::CreateSP(this, &FDataTableEditor::CanEditTable)), NAME_None, LOCTEXT("PasteIconText", "Paste"), LOCTEXT("PasteToolTip", "Paste on the currently selected row"), FSlateIcon(FAppStyle::GetAppStyleSetName(), "GenericCommands.Paste")); ToolbarBuilder.AddToolBarButton( FUIAction( FExecuteAction::CreateSP(this, &FDataTableEditor::OnDuplicateClicked), FCanExecuteAction::CreateSP(this, &FDataTableEditor::CanEditTable)), NAME_None, LOCTEXT("DuplicateIconText", "Duplicate"), LOCTEXT("DuplicateToolTip", "Duplicate the currently selected row"), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Duplicate")); ToolbarBuilder.AddToolBarButton( FUIAction( FExecuteAction::CreateSP(this, &FDataTableEditor::OnRemoveClicked), FCanExecuteAction::CreateSP(this, &FDataTableEditor::CanEditTable)), NAME_None, LOCTEXT("RemoveRowIconText", "Remove"), LOCTEXT("RemoveRowToolTip", "Remove the currently selected row from the Data Table"), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Delete")); } ToolbarBuilder.EndSection(); } UDataTable* FDataTableEditor::GetEditableDataTable() const { return Cast(GetEditingObject()); } FText FDataTableEditor::GetBaseToolkitName() const { return LOCTEXT( "AppLabel", "DataTable Editor" ); } FString FDataTableEditor::GetWorldCentricTabPrefix() const { return LOCTEXT("WorldCentricTabPrefix", "DataTable ").ToString(); } FLinearColor FDataTableEditor::GetWorldCentricTabColorScale() const { return FLinearColor( 0.0f, 0.0f, 0.2f, 0.5f ); } FSlateColor FDataTableEditor::GetRowTextColor(FName RowName) const { if (RowName == HighlightedRowName) { return FSlateColor(FColorList::Orange); } return FSlateColor::UseForeground(); } FText FDataTableEditor::GetCellText(FDataTableEditorRowListViewDataPtr InRowDataPointer, int32 ColumnIndex) const { if (InRowDataPointer.IsValid() && ColumnIndex < InRowDataPointer->CellData.Num()) { return InRowDataPointer->CellData[ColumnIndex]; } return FText(); } FText FDataTableEditor::GetCellToolTipText(FDataTableEditorRowListViewDataPtr InRowDataPointer, int32 ColumnIndex) const { FText TooltipText; if (ColumnIndex < AvailableColumns.Num()) { TooltipText = AvailableColumns[ColumnIndex]->DisplayName; } if (InRowDataPointer.IsValid() && ColumnIndex < InRowDataPointer->CellData.Num()) { TooltipText = FText::Format(LOCTEXT("ColumnRowNameFmt", "{0}: {1}"), TooltipText, InRowDataPointer->CellData[ColumnIndex]); } return TooltipText; } float FDataTableEditor::GetRowNumberColumnWidth() const { return RowNumberColumnWidth; } void FDataTableEditor::RefreshRowNumberColumnWidth() { TSharedRef FontMeasure = FSlateApplication::Get().GetRenderer()->GetFontMeasureService(); const FTextBlockStyle& CellTextStyle = FAppStyle::GetWidgetStyle("DataTableEditor.CellText"); const float CellPadding = 10.0f; for (const FDataTableEditorRowListViewDataPtr& RowData : AvailableRows) { const float RowNumberWidth = (float)FontMeasure->Measure(FString::FromInt(RowData->RowNum), CellTextStyle.Font).X + CellPadding; RowNumberColumnWidth = FMath::Max(RowNumberColumnWidth, RowNumberWidth); } } void FDataTableEditor::OnRowNumberColumnResized(const float NewWidth) { RowNumberColumnWidth = NewWidth; } float FDataTableEditor::GetRowNameColumnWidth() const { return RowNameColumnWidth; } void FDataTableEditor::RefreshRowNameColumnWidth() { TSharedRef FontMeasure = FSlateApplication::Get().GetRenderer()->GetFontMeasureService(); const FTextBlockStyle& CellTextStyle = FAppStyle::GetWidgetStyle("DataTableEditor.CellText"); static const float CellPadding = 10.0f; for (const FDataTableEditorRowListViewDataPtr& RowData : AvailableRows) { const float RowNameWidth = (float)FontMeasure->Measure(RowData->DisplayName, CellTextStyle.Font).X + CellPadding; RowNameColumnWidth = FMath::Max(RowNameColumnWidth, RowNameWidth); } } float FDataTableEditor::GetColumnWidth(const int32 ColumnIndex) const { if (ColumnWidths.IsValidIndex(ColumnIndex)) { return ColumnWidths[ColumnIndex].CurrentWidth; } return 0.0f; } void FDataTableEditor::OnColumnResized(const float NewWidth, const int32 ColumnIndex) { if (ColumnWidths.IsValidIndex(ColumnIndex)) { FColumnWidth& ColumnWidth = ColumnWidths[ColumnIndex]; ColumnWidth.bIsAutoSized = false; ColumnWidth.CurrentWidth = NewWidth; // Update the persistent column widths in the layout data { if (!LayoutData.IsValid()) { LayoutData = MakeShareable(new FJsonObject()); } TSharedPtr LayoutColumnWidths; if (!LayoutData->HasField(TEXT("ColumnWidths"))) { LayoutColumnWidths = MakeShareable(new FJsonObject()); LayoutData->SetObjectField(TEXT("ColumnWidths"), LayoutColumnWidths); } else { LayoutColumnWidths = LayoutData->GetObjectField(TEXT("ColumnWidths")); } const FString& ColumnName = AvailableColumns[ColumnIndex]->ColumnId.ToString(); LayoutColumnWidths->SetNumberField(ColumnName, NewWidth); } } } void FDataTableEditor::OnRowNameColumnResized(const float NewWidth) { RowNameColumnWidth = NewWidth; } void FDataTableEditor::LoadLayoutData() { LayoutData.Reset(); const UDataTable* Table = GetDataTable(); if (!Table) { return; } const FString LayoutDataFilename = FPaths::ProjectSavedDir() / TEXT("AssetData") / TEXT("DataTableEditorLayout") / Table->GetName() + TEXT(".json"); FString JsonText; if (FFileHelper::LoadFileToString(JsonText, *LayoutDataFilename)) { TSharedRef< TJsonReader > JsonReader = TJsonReaderFactory::Create(JsonText); FJsonSerializer::Deserialize(JsonReader, LayoutData); } } void FDataTableEditor::SaveLayoutData() { const UDataTable* Table = GetDataTable(); if (!Table || !LayoutData.IsValid()) { return; } const FString LayoutDataFilename = FPaths::ProjectSavedDir() / TEXT("AssetData") / TEXT("DataTableEditorLayout") / Table->GetName() + TEXT(".json"); FString JsonText; TSharedRef< TJsonWriter< TCHAR, TPrettyJsonPrintPolicy > > JsonWriter = TJsonWriterFactory< TCHAR, TPrettyJsonPrintPolicy >::Create(&JsonText); if (FJsonSerializer::Serialize(LayoutData.ToSharedRef(), JsonWriter)) { FFileHelper::SaveStringToFile(JsonText, *LayoutDataFilename); } } TSharedRef FDataTableEditor::MakeRowWidget(FDataTableEditorRowListViewDataPtr InRowDataPtr, const TSharedRef& OwnerTable) { return SNew(SDataTableListViewRow, OwnerTable) .DataTableEditor(SharedThis(this)) .RowDataPtr(InRowDataPtr) .IsEditable(CanEditRows()); } TSharedRef FDataTableEditor::MakeCellWidget(FDataTableEditorRowListViewDataPtr InRowDataPtr, const int32 InRowIndex, const FName& InColumnId) { int32 ColumnIndex = 0; for (; ColumnIndex < AvailableColumns.Num(); ++ColumnIndex) { const FDataTableEditorColumnHeaderDataPtr& ColumnData = AvailableColumns[ColumnIndex]; if (ColumnData->ColumnId == InColumnId) { break; } } // Valid column ID? if (AvailableColumns.IsValidIndex(ColumnIndex) && InRowDataPtr->CellData.IsValidIndex(ColumnIndex)) { return SNew(SBox) .Padding(FMargin(4, 2, 4, 2)) [ SNew(STextBlock) .TextStyle(FAppStyle::Get(), "DataTableEditor.CellText") .ColorAndOpacity(this, &FDataTableEditor::GetRowTextColor, InRowDataPtr->RowId) .Text(this, &FDataTableEditor::GetCellText, InRowDataPtr, ColumnIndex) .HighlightText(this, &FDataTableEditor::GetFilterText) .ToolTipText(this, &FDataTableEditor::GetCellToolTipText, InRowDataPtr, ColumnIndex) ]; } return SNullWidget::NullWidget; } void FDataTableEditor::OnRowSelectionChanged(FDataTableEditorRowListViewDataPtr InNewSelection, ESelectInfo::Type InSelectInfo) { const bool bSelectionChanged = !InNewSelection.IsValid() || InNewSelection->RowId != HighlightedRowName; const FName NewRowName = (InNewSelection.IsValid()) ? InNewSelection->RowId : NAME_None; SetHighlightedRow(NewRowName); if (bSelectionChanged) { CallbackOnRowHighlighted.ExecuteIfBound(HighlightedRowName); } } void FDataTableEditor::CopySelectedRow() { UDataTable* TablePtr = Cast(GetEditingObject()); uint8* RowPtr = TablePtr ? TablePtr->GetRowMap().FindRef(HighlightedRowName) : nullptr; if (!RowPtr || !TablePtr->RowStruct) return; FString ClipboardValue; TablePtr->RowStruct->ExportText(ClipboardValue, RowPtr, RowPtr, TablePtr, PPF_Copy, nullptr); FPlatformApplicationMisc::ClipboardCopy(*ClipboardValue); } void FDataTableEditor::PasteOnSelectedRow() { UDataTable* TablePtr = Cast(GetEditingObject()); uint8* RowPtr = TablePtr ? TablePtr->GetRowMap().FindRef(HighlightedRowName) : nullptr; if (!RowPtr || !TablePtr->RowStruct) return; const FScopedTransaction Transaction(LOCTEXT("PasteDataTableRow", "Paste Data Table Row")); TablePtr->Modify(); FString ClipboardValue; FPlatformApplicationMisc::ClipboardPaste(ClipboardValue); FDataTableEditorUtils::BroadcastPreChange(TablePtr, FDataTableEditorUtils::EDataTableChangeInfo::RowData); const TCHAR* Result = TablePtr->RowStruct->ImportText(*ClipboardValue, RowPtr, TablePtr, PPF_Copy, GWarn, GetPathNameSafe(TablePtr->RowStruct)); TablePtr->HandleDataTableChanged(HighlightedRowName); TablePtr->MarkPackageDirty(); FDataTableEditorUtils::BroadcastPostChange(TablePtr, FDataTableEditorUtils::EDataTableChangeInfo::RowData); if (Result == nullptr) { FNotificationInfo Info(LOCTEXT("FailedPaste", "Failed to paste row")); FSlateNotificationManager::Get().AddNotification(Info); } } void FDataTableEditor::DuplicateSelectedRow() { UDataTable* TablePtr = Cast(GetEditingObject()); FName NewName = HighlightedRowName; if (NewName == NAME_None || TablePtr == nullptr) { return; } const TArray ExistingNames = TablePtr->GetRowNames(); while (ExistingNames.Contains(NewName)) { NewName.SetNumber(NewName.GetNumber() + 1); } FDataTableEditorUtils::DuplicateRow(TablePtr, HighlightedRowName, NewName); FDataTableEditorUtils::SelectRow(TablePtr, NewName); } void FDataTableEditor::RenameSelectedRowCommand() { UDataTable* TablePtr = Cast(GetEditingObject()); FName NewName = HighlightedRowName; if (NewName == NAME_None || TablePtr == nullptr) { return; } if (VisibleRows.IsValidIndex(HighlightedVisibleRowIndex)) { TSharedPtr< SDataTableListViewRow > RowWidget = StaticCastSharedPtr< SDataTableListViewRow >(CellsListView->WidgetFromItem(VisibleRows[HighlightedVisibleRowIndex])); RowWidget->SetRowForRename(); } } void FDataTableEditor::DeleteSelectedRow() { if (UDataTable* Table = GetEditableDataTable()) { // We must perform this before removing the row const int32 RowToRemoveIndex = VisibleRows.IndexOfByPredicate([&](const FDataTableEditorRowListViewDataPtr& InRowName) -> bool { return InRowName->RowId == HighlightedRowName; }); // Remove row if (FDataTableEditorUtils::RemoveRow(Table, HighlightedRowName)) { // Try and keep the same row index selected const int32 RowIndexToSelect = FMath::Clamp(RowToRemoveIndex, 0, VisibleRows.Num() - 1); if (VisibleRows.IsValidIndex(RowIndexToSelect)) { FDataTableEditorUtils::SelectRow(Table, VisibleRows[RowIndexToSelect]->RowId); } // Refresh list. Otherwise, the removed row would still appear in the screen until the next list refresh. An // analog of CellsListView->RequestListRefresh() also occurs inside FDataTableEditorUtils::SelectRow else { CellsListView->RequestListRefresh(); } } } } FText FDataTableEditor::GetFilterText() const { return ActiveFilterText; } void FDataTableEditor::OnFilterTextChanged(const FText& InFilterText) { ActiveFilterText = InFilterText; UpdateVisibleRows(); } void FDataTableEditor::OnFilterTextCommitted(const FText& NewText, ETextCommit::Type CommitInfo) { if (CommitInfo == ETextCommit::OnCleared) { SearchBoxWidget->SetText(FText::GetEmpty()); OnFilterTextChanged(FText::GetEmpty()); } } void FDataTableEditor::PostRegenerateMenusAndToolbars() { const UDataTable* DataTable = GetDataTable(); if (DataTable) { const UUserDefinedStruct* UDS = Cast(DataTable->GetRowStruct()); // build and attach the menu overlay TSharedRef MenuOverlayBox = SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(STextBlock) .ColorAndOpacity(FSlateColor::UseSubduedForeground()) .ShadowOffset(FVector2D::UnitVector) .Text(LOCTEXT("DataTableEditor_RowStructType", "Row Type: ")) ] + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(SHyperlink) .Style(FAppStyle::Get(), "Common.GotoNativeCodeHyperlink") .OnNavigate(this, &FDataTableEditor::OnEditDataTableStructClicked) .Text(FText::FromName(DataTable->GetRowStructPathName().GetAssetName())) .ToolTipText(LOCTEXT("DataTableRowToolTip", "Open the struct used for each row in this data table")) ] + SHorizontalBox::Slot() .AutoWidth() [ SNew(SButton) .VAlign(VAlign_Center) .ButtonStyle(FAppStyle::Get(), "HoverHintOnly") .OnClicked(this, &FDataTableEditor::OnFindRowInContentBrowserClicked) .Visibility(UDS ? EVisibility::Visible : EVisibility::Collapsed) .ToolTipText(LOCTEXT("FindRowInCBToolTip", "Find struct in Content Browser")) .ContentPadding(4.0f) .ForegroundColor(FSlateColor::UseForeground()) [ SNew(SImage) .Image(FAppStyle::GetBrush("Icons.Search")) ] ]; SetMenuOverlay(MenuOverlayBox); } } FReply FDataTableEditor::OnFindRowInContentBrowserClicked() { const UDataTable* DataTable = GetDataTable(); if(DataTable) { TArray ObjectsToSync; ObjectsToSync.Add(FAssetData(DataTable->GetRowStruct())); GEditor->SyncBrowserToObjects(ObjectsToSync); } return FReply::Handled(); } void FDataTableEditor::RefreshCachedDataTable(const FName InCachedSelection, const bool bUpdateEvenIfValid) { UDataTable* Table = GetEditableDataTable(); TArray PreviousColumns = AvailableColumns; FDataTableEditorUtils::CacheDataTableForEditing(Table, AvailableColumns, AvailableRows); // Update the desired width of the row names and numbers column // This prevents it growing or shrinking as you scroll the list view RefreshRowNumberColumnWidth(); RefreshRowNameColumnWidth(); // Setup the default auto-sized columns ColumnWidths.SetNum(AvailableColumns.Num()); for (int32 ColumnIndex = 0; ColumnIndex < AvailableColumns.Num(); ++ColumnIndex) { const FDataTableEditorColumnHeaderDataPtr& ColumnData = AvailableColumns[ColumnIndex]; FColumnWidth& ColumnWidth = ColumnWidths[ColumnIndex]; ColumnWidth.CurrentWidth = FMath::Clamp(ColumnData->DesiredColumnWidth, 10.0f, 400.0f); // Clamp auto-sized columns to a reasonable limit } // Load the persistent column widths from the layout data { const TSharedPtr* LayoutColumnWidths = nullptr; if (LayoutData.IsValid() && LayoutData->TryGetObjectField(TEXT("ColumnWidths"), LayoutColumnWidths)) { for(int32 ColumnIndex = 0; ColumnIndex < AvailableColumns.Num(); ++ColumnIndex) { const FDataTableEditorColumnHeaderDataPtr& ColumnData = AvailableColumns[ColumnIndex]; double LayoutColumnWidth = 0.0f; if ((*LayoutColumnWidths)->TryGetNumberField(ColumnData->ColumnId.ToString(), LayoutColumnWidth)) { FColumnWidth& ColumnWidth = ColumnWidths[ColumnIndex]; ColumnWidth.bIsAutoSized = false; ColumnWidth.CurrentWidth = static_cast(LayoutColumnWidth); } } } } if (PreviousColumns != AvailableColumns) { ColumnNamesHeaderRow->ClearColumns(); if (CanEditRows()) { ColumnNamesHeaderRow->AddColumn( SHeaderRow::Column(RowDragDropColumnId) [ SNew(SBox) .VAlign(VAlign_Fill) .HAlign(HAlign_Fill) .ToolTip(IDocumentation::Get()->CreateToolTip( LOCTEXT("DataTableRowHandleTooltip", "Drag Drop Handles"), nullptr, *FDataTableEditorUtils::VariableTypesTooltipDocLink, TEXT("DataTableRowHandle"))) [ SNew(STextBlock) .Text(FText::GetEmpty()) ] ] ); } ColumnNamesHeaderRow->AddColumn( SHeaderRow::Column(RowNumberColumnId) .SortMode(this, &FDataTableEditor::GetColumnSortMode, RowNumberColumnId) .OnSort(this, &FDataTableEditor::OnColumnNumberSortModeChanged) .ManualWidth(this, &FDataTableEditor::GetRowNumberColumnWidth) .OnWidthChanged(this, &FDataTableEditor::OnRowNumberColumnResized) [ SNew(SBox) .VAlign(VAlign_Fill) .HAlign(HAlign_Fill) .ToolTip(IDocumentation::Get()->CreateToolTip( LOCTEXT("DataTableRowIndexTooltip", "Row Index"), nullptr, *FDataTableEditorUtils::VariableTypesTooltipDocLink, TEXT("DataTableRowIndex"))) [ SNew(STextBlock) .Text(FText::GetEmpty()) ] ] ); ColumnNamesHeaderRow->AddColumn( SHeaderRow::Column(RowNameColumnId) .DefaultLabel(LOCTEXT("DataTableRowName", "Row Name")) .ManualWidth(this, &FDataTableEditor::GetRowNameColumnWidth) .OnWidthChanged(this, &FDataTableEditor::OnRowNameColumnResized) .SortMode(this, &FDataTableEditor::GetColumnSortMode, RowNameColumnId) .OnSort(this, &FDataTableEditor::OnColumnNameSortModeChanged) ); for (int32 ColumnIndex = 0; ColumnIndex < AvailableColumns.Num(); ++ColumnIndex) { const FDataTableEditorColumnHeaderDataPtr& ColumnData = AvailableColumns[ColumnIndex]; ColumnNamesHeaderRow->AddColumn( SHeaderRow::Column(ColumnData->ColumnId) .DefaultLabel(ColumnData->DisplayName) .ManualWidth(TAttribute::Create(TAttribute::FGetter::CreateSP(this, &FDataTableEditor::GetColumnWidth, ColumnIndex))) .OnWidthChanged(this, &FDataTableEditor::OnColumnResized, ColumnIndex) .SortMode(this, &FDataTableEditor::GetColumnSortMode, ColumnData->ColumnId) .OnSort(this, &FDataTableEditor::OnColumnSortModeChanged) [ SNew(SBox) .Padding(FMargin(0, 4, 0, 4)) .VAlign(VAlign_Fill) .ToolTip(IDocumentation::Get()->CreateToolTip(FDataTableEditorUtils::GetRowTypeInfoTooltipText(ColumnData), nullptr, *FDataTableEditorUtils::VariableTypesTooltipDocLink, FDataTableEditorUtils::GetRowTypeTooltipDocExcerptName(ColumnData))) [ SNew(STextBlock) .Justification(ETextJustify::Center) .Text(ColumnData->DisplayName) ] ] ); } } UpdateVisibleRows(InCachedSelection, bUpdateEvenIfValid); if (PropertyView.IsValid()) { PropertyView->SetObject(Table); } } void FDataTableEditor::UpdateVisibleRows(const FName InCachedSelection, const bool bUpdateEvenIfValid) { if (ActiveFilterText.IsEmptyOrWhitespace()) { VisibleRows = AvailableRows; } else { VisibleRows.Empty(AvailableRows.Num()); const FString& ActiveFilterString = ActiveFilterText.ToString(); for (const FDataTableEditorRowListViewDataPtr& RowData : AvailableRows) { bool bPassesFilter = false; if (RowData->DisplayName.ToString().Contains(ActiveFilterString)) { bPassesFilter = true; } else { for (const FText& CellText : RowData->CellData) { if (CellText.ToString().Contains(ActiveFilterString)) { bPassesFilter = true; break; } } } if (bPassesFilter) { VisibleRows.Add(RowData); } } } CellsListView->RequestListRefresh(); RestoreCachedSelection(InCachedSelection, bUpdateEvenIfValid); } void FDataTableEditor::RestoreCachedSelection(const FName InCachedSelection, const bool bUpdateEvenIfValid) { // Validate the requested selection to see if it matches a known row bool bSelectedRowIsValid = false; if (!InCachedSelection.IsNone()) { bSelectedRowIsValid = VisibleRows.ContainsByPredicate([&InCachedSelection](const FDataTableEditorRowListViewDataPtr& RowData) -> bool { return RowData->RowId == InCachedSelection; }); } // Apply the new selection (if required) if (!bSelectedRowIsValid) { SetHighlightedRow((VisibleRows.Num() > 0) ? VisibleRows[0]->RowId : NAME_None); CallbackOnRowHighlighted.ExecuteIfBound(HighlightedRowName); } else if (bUpdateEvenIfValid) { SetHighlightedRow(InCachedSelection); CallbackOnRowHighlighted.ExecuteIfBound(HighlightedRowName); } } TSharedRef FDataTableEditor::CreateContentBox() { TSharedRef HorizontalScrollBar = SNew(SScrollBar) .Orientation(Orient_Horizontal) .Thickness(FVector2D(12.0f, 12.0f)); TSharedRef VerticalScrollBar = SNew(SScrollBar) .Orientation(Orient_Vertical) .Thickness(FVector2D(12.0f, 12.0f)); ColumnNamesHeaderRow = SNew(SHeaderRow); CellsListView = SNew(SListView) .ListItemsSource(&VisibleRows) .HeaderRow(ColumnNamesHeaderRow) .OnGenerateRow(this, &FDataTableEditor::MakeRowWidget) .OnSelectionChanged(this, &FDataTableEditor::OnRowSelectionChanged) .ExternalScrollbar(VerticalScrollBar) .ConsumeMouseWheel(EConsumeMouseWheel::Always) .SelectionMode(ESelectionMode::Single) .AllowOverscroll(EAllowOverscroll::No); LoadLayoutData(); RefreshCachedDataTable(); return SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() [ SAssignNew(SearchBoxWidget, SSearchBox) .InitialText(this, &FDataTableEditor::GetFilterText) .OnTextChanged(this, &FDataTableEditor::OnFilterTextChanged) .OnTextCommitted(this, &FDataTableEditor::OnFilterTextCommitted) ] ] +SVerticalBox::Slot() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() [ SNew(SScrollBox) .Orientation(Orient_Horizontal) .ExternalScrollbar(HorizontalScrollBar) +SScrollBox::Slot() [ CellsListView.ToSharedRef() ] ] +SHorizontalBox::Slot() .AutoWidth() [ VerticalScrollBar ] ] +SVerticalBox::Slot() .AutoHeight() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() [ HorizontalScrollBar ] ]; } TSharedRef FDataTableEditor::CreateRowEditorBox() { UDataTable* Table = Cast(GetEditingObject()); // Support undo/redo if (Table) { Table->SetFlags(RF_Transactional); } auto RowEditor = SNew(SRowEditor, Table); RowEditor->RowSelectedCallback.BindSP(this, &FDataTableEditor::SetHighlightedRow); CallbackOnRowHighlighted.BindSP(RowEditor, &SRowEditor::SelectRow); CallbackOnDataTableUndoRedo.BindSP(RowEditor, &SRowEditor::HandleUndoRedo); return RowEditor; } TSharedRef FDataTableEditor::CreateRowEditor(UDataTable* Table) { return SNew(SRowEditor, Table); } TSharedRef FDataTableEditor::SpawnTab_RowEditor(const FSpawnTabArgs& Args) { check(Args.GetTabId().TabType == RowEditorTabId); return SNew(SDockTab) .Label(LOCTEXT("RowEditorTitle", "Row Editor")) .TabColorScale(GetTabColorScale()) [ SNew(SBorder) .Padding(2) .VAlign(VAlign_Top) .HAlign(HAlign_Fill) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ RowEditorTabWidget.ToSharedRef() ] ]; } TSharedRef FDataTableEditor::SpawnTab_DataTable( const FSpawnTabArgs& Args ) { check( Args.GetTabId().TabType == DataTableTabId ); UDataTable* Table = Cast(GetEditingObject()); // Support undo/redo if (Table) { Table->SetFlags(RF_Transactional); } return SNew(SDockTab) .Label( LOCTEXT("DataTableTitle", "Data Table") ) .TabColorScale( GetTabColorScale() ) [ SNew(SBorder) .Padding(2) .BorderImage( FAppStyle::GetBrush( "ToolPanel.GroupBorder" ) ) [ DataTableTabWidget.ToSharedRef() ] ]; } TSharedRef FDataTableEditor::SpawnTab_DataTableDetails(const FSpawnTabArgs& Args) { check(Args.GetTabId().TabType == DataTableDetailsTabId); PropertyView->SetObject(GetEditableDataTable()); return SNew(SDockTab) .Label(LOCTEXT("DataTableDetails", "Data Table Details")) .TabColorScale(GetTabColorScale()) [ SNew(SBorder) .Padding(2) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ PropertyView.ToSharedRef() ] ]; } void FDataTableEditor::SetHighlightedRow(FName Name) { if (Name == HighlightedRowName) { return; } if (Name.IsNone()) { HighlightedRowName = NAME_None; CellsListView->ClearSelection(); HighlightedVisibleRowIndex = INDEX_NONE; } else { HighlightedRowName = Name; FDataTableEditorRowListViewDataPtr* NewSelectionPtr = NULL; for (HighlightedVisibleRowIndex = 0; HighlightedVisibleRowIndex < VisibleRows.Num(); ++HighlightedVisibleRowIndex) { if (VisibleRows[HighlightedVisibleRowIndex]->RowId == Name) { NewSelectionPtr = &(VisibleRows[HighlightedVisibleRowIndex]); break; } } // Synchronize the list views if (NewSelectionPtr) { CellsListView->SetSelection(*NewSelectionPtr); CellsListView->RequestScrollIntoView(*NewSelectionPtr); } else { CellsListView->ClearSelection(); } } } #undef LOCTEXT_NAMESPACE