// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "DataTableEditor.h" #include "DataTableEditorModule.h" #include "Dom/JsonObject.h" #include "Editor.h" #include "EditorStyleSet.h" #include "Fonts/FontMeasure.h" #include "Framework/Application/SlateApplication.h" #include "Framework/Commands/GenericCommands.h" #include "Framework/Layout/Overscroll.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Framework/Notifications/NotificationManager.h" #include "HAL/PlatformApplicationMisc.h" #include "IDocumentation.h" #include "Misc/FeedbackContext.h" #include "Misc/FileHelper.h" #include "Modules/ModuleManager.h" #include "Policies/PrettyJsonPrintPolicy.h" #include "ScopedTransaction.h" #include "SDataTableListViewRowName.h" #include "Serialization/JsonReader.h" #include "Serialization/JsonSerializer.h" #include "Widgets/Docking/SDockTab.h" #include "Widgets/Input/SSearchBox.h" #include "Widgets/Layout/SScrollBar.h" #include "Widgets/Layout/SScrollBox.h" #include "Widgets/SToolTip.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Views/SListView.h" #include "Widgets/Input/SButton.h" #include "Widgets/Images/SImage.h" #include "Widgets/Input/SHyperlink.h" #include "Widgets/Notifications/SNotificationList.h" #include "SourceCodeNavigation.h" #include "PropertyEditorModule.h" #include "UObject/StructOnScope.h" #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"); class SDataTableListViewRow : public SMultiColumnTableRow { public: SLATE_BEGIN_ARGS(SDataTableListViewRow) {} /** The widget that owns the tree. We'll only keep a weak reference to it. */ SLATE_ARGUMENT(TSharedPtr, DataTableEditor) /** The list item for this row */ SLATE_ARGUMENT(FDataTableEditorRowListViewDataPtr, Item) SLATE_END_ARGS() /** Construct function for this widget */ void Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView) { DataTableEditor = InArgs._DataTableEditor; Item = InArgs._Item; SMultiColumnTableRow::Construct( FSuperRowType::FArguments() .Style(FEditorStyle::Get(), "DataTableEditor.CellListViewRow"), InOwnerTableView ); } /** Overridden from SMultiColumnTableRow. Generates a widget for this column of the list view. */ virtual TSharedRef GenerateWidgetForColumn(const FName& ColumnName) override { TSharedPtr DataTableEditorPtr = DataTableEditor.Pin(); return (DataTableEditorPtr.IsValid()) ? DataTableEditorPtr->MakeCellWidget(Item, IndexInList, ColumnName) : SNullWidget::NullWidget; } private: /** Weak reference to the data table editor that owns our list */ TWeakPtr DataTableEditor; /** The item associated with this row of data */ FDataTableEditorRowListViewDataPtr Item; }; 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(/*bUpdateFromSelection=*/ false, /*bLockable=*/ false, /*bAllowSearch=*/ true, /*InNameAreaSettings=*/ FDetailsViewArgs::HideNameArea, /*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() { } FDataTableEditor::~FDataTableEditor() { GEditor->UnregisterForUndo(this); const UDataTable* Table = GetDataTable(); 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) { const UDataTable* Table = GetDataTable(); if (Changed == Table) { HandlePostChange(); const_cast(Table)->OnDataTableChanged().Broadcast(); } } 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_v3" ) ->AddArea ( FTabManager::NewPrimaryArea()->SetOrientation(Orient_Vertical) ->Split ( FTabManager::NewStack() ->SetSizeCoefficient(0.1f) ->SetHideTabWell(true) ->AddTab(GetToolbarTabId(), ETabState::OpenedTab) ) ->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())); 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)); ToolkitCommands->MapAction(FGenericCommands::Get().Paste, FExecuteAction::CreateSP(this, &FDataTableEditor::PasteOnSelectedRow)); ToolkitCommands->MapAction(FGenericCommands::Get().Duplicate, FExecuteAction::CreateSP(this, &FDataTableEditor::DuplicateSelectedRow)); } FName FDataTableEditor::GetToolkitFName() const { return FName("DataTableEditor"); } 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; } FOptionalSize FDataTableEditor::GetRowNameColumnWidth() const { return FOptionalSize(RowNameColumnWidth); } 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::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::MakeRowNameWidget(FDataTableEditorRowListViewDataPtr InRowDataPtr, const TSharedRef& OwnerTable) { return SNew(SDataTableListViewRowName, OwnerTable) .DataTableEditor(SharedThis(this)) .RowDataPtr(InRowDataPtr); } TSharedRef FDataTableEditor::MakeRowWidget(FDataTableEditorRowListViewDataPtr InRowDataPtr, const TSharedRef& OwnerTable) { return SNew(SDataTableListViewRow, OwnerTable) .DataTableEditor(SharedThis(this)) .Item(InRowDataPtr); } 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(FEditorStyle::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::OnRowNamesListViewScrolled(double InScrollOffset) { // Synchronize the list views CellsListView->SetScrollOffset(InScrollOffset); } void FDataTableEditor::OnCellsListViewScrolled(double InScrollOffset) { // Synchronize the list views RowNamesListView->SetScrollOffset(InScrollOffset); } 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)); 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); } FText FDataTableEditor::GetFilterText() const { return ActiveFilterText; } void FDataTableEditor::OnFilterTextChanged(const FText& InFilterText) { ActiveFilterText = InFilterText; UpdateVisibleRows(); } 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(STextBlock) .ShadowOffset(FVector2D::UnitVector) .Text(FText::FromName(DataTable->GetRowStructName())) .ToolTipText(LOCTEXT("DataTableRowToolTip", "The struct used for each row in this data table")) .Visibility(UDS ? EVisibility::Visible : EVisibility::Collapsed) ] + SHorizontalBox::Slot() .AutoWidth() [ SNew(SButton) .VAlign(VAlign_Center) .ButtonStyle(FEditorStyle::Get(), "HoverHintOnly") .OnClicked(this, &FDataTableEditor::OnFindRowInContentBrowserClicked) .Visibility(UDS ? EVisibility::Visible : EVisibility::Collapsed) .ToolTipText(LOCTEXT("FindRowInCBToolTip", "Find row in Content Browser")) .ContentPadding(4.0f) .ForegroundColor(FSlateColor::UseForeground()) [ SNew(SImage) .Image(FEditorStyle::GetBrush("PropertyWindow.Button_Browse")) ] ] +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(0.0f, 0.0f, 8.0f, 0.0f) [ SNew(SHyperlink) .Style(FEditorStyle::Get(), "Common.GotoNativeCodeHyperlink") .Visibility(!UDS ? EVisibility::Visible : EVisibility::Collapsed) .OnNavigate(this, &FDataTableEditor::OnNavigateToDataTableRowCode) .Text(FText::FromName(DataTable->GetRowStructName())) .ToolTipText(FText::Format(LOCTEXT("GoToCode_ToolTip", "Click to open this source file in {0}"), FSourceCodeNavigation::GetSelectedSourceCodeIDE())) ]; 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::OnNavigateToDataTableRowCode() { const UDataTable* DataTable = GetDataTable(); if (DataTable && FSourceCodeNavigation::NavigateToStruct(DataTable->GetRowStruct())) { FSourceCodeNavigation::NavigateToStruct(DataTable->GetRowStruct()); } } void FDataTableEditor::RefreshCachedDataTable(const FName InCachedSelection, const bool bUpdateEvenIfValid) { const UDataTable* Table = GetDataTable(); TArray PreviousColumns = AvailableColumns; FDataTableEditorUtils::CacheDataTableForEditing(Table, AvailableColumns, AvailableRows); // Update the desired width of the row names column // This prevents it growing or shrinking as you scroll the list view { TSharedRef FontMeasure = FSlateApplication::Get().GetRenderer()->GetFontMeasureService(); const FTextBlockStyle& CellTextStyle = FEditorStyle::GetWidgetStyle("DataTableEditor.CellText"); static const float CellPadding = 10.0f; RowNameColumnWidth = 10.0f; for (const FDataTableEditorRowListViewDataPtr& RowData : AvailableRows) { const float RowNameWidth = FontMeasure->Measure(RowData->DisplayName, CellTextStyle.Font).X + CellPadding; RowNameColumnWidth = FMath::Max(RowNameColumnWidth, RowNameWidth); } } // 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(); 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) [ 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(const_cast(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); } } } // Abort restoring the cached selection if data was changed while the user is selecting a different row if (RowNamesListView->GetSelectedItems() == CellsListView->GetSelectedItems()) { RowNamesListView->RequestListRefresh(); 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)); TSharedRef RowNamesHeaderRow = SNew(SHeaderRow); RowNamesHeaderRow->AddColumn( SHeaderRow::Column(RowNameColumnId) .DefaultLabel(FText::GetEmpty()) ); ColumnNamesHeaderRow = SNew(SHeaderRow); RowNamesListView = SNew(SListView) .ListItemsSource(&VisibleRows) .HeaderRow(RowNamesHeaderRow) .OnGenerateRow(this, &FDataTableEditor::MakeRowNameWidget) .OnListViewScrolled(this, &FDataTableEditor::OnRowNamesListViewScrolled) .OnSelectionChanged(this, &FDataTableEditor::OnRowSelectionChanged) .ScrollbarVisibility(EVisibility::Collapsed) .ConsumeMouseWheel(EConsumeMouseWheel::Always) .SelectionMode(ESelectionMode::Single) .AllowOverscroll(EAllowOverscroll::No); CellsListView = SNew(SListView) .ListItemsSource(&VisibleRows) .HeaderRow(ColumnNamesHeaderRow) .OnGenerateRow(this, &FDataTableEditor::MakeRowWidget) .OnListViewScrolled(this, &FDataTableEditor::OnCellsListViewScrolled) .OnSelectionChanged(this, &FDataTableEditor::OnRowSelectionChanged) .ExternalScrollbar(VerticalScrollBar) .ConsumeMouseWheel(EConsumeMouseWheel::Always) .SelectionMode(ESelectionMode::Single) .AllowOverscroll(EAllowOverscroll::No); RefreshCachedDataTable(); return SNew(SVerticalBox) +SVerticalBox::Slot() .AutoHeight() [ SNew(SSearchBox) .InitialText(this, &FDataTableEditor::GetFilterText) .OnTextChanged(this, &FDataTableEditor::OnFilterTextChanged) ] +SVerticalBox::Slot() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() [ SNew(SBox) .WidthOverride(this, &FDataTableEditor::GetRowNameColumnWidth) [ RowNamesListView.ToSharedRef() ] ] +SHorizontalBox::Slot() [ SNew(SScrollBox) .Orientation(Orient_Horizontal) .ExternalScrollbar(HorizontalScrollBar) +SScrollBox::Slot() [ CellsListView.ToSharedRef() ] ] +SHorizontalBox::Slot() .AutoWidth() [ VerticalScrollBar ] ] +SVerticalBox::Slot() .AutoHeight() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() [ SNew(SBox) .WidthOverride(this, &FDataTableEditor::GetRowNameColumnWidth) [ SNullWidget::NullWidget ] ] +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) .Icon(FEditorStyle::GetBrush("DataTableEditor.Tabs.Properties")) .Label(LOCTEXT("RowEditorTitle", "Row Editor")) .TabColorScale(GetTabColorScale()) [ SNew(SBorder) .Padding(2) .VAlign(VAlign_Top) .HAlign(HAlign_Fill) .BorderImage(FEditorStyle::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); } LoadLayoutData(); return SNew(SDockTab) .Icon( FEditorStyle::GetBrush("DataTableEditor.Tabs.Properties") ) .Label( LOCTEXT("DataTableTitle", "Data Table") ) .TabColorScale( GetTabColorScale() ) [ SNew(SBorder) .Padding(2) .BorderImage( FEditorStyle::GetBrush( "ToolPanel.GroupBorder" ) ) [ DataTableTabWidget.ToSharedRef() ] ]; } TSharedRef FDataTableEditor::SpawnTab_DataTableDetails(const FSpawnTabArgs& Args) { check(Args.GetTabId().TabType == DataTableDetailsTabId); PropertyView->SetObject(const_cast(GetDataTable())); return SNew(SDockTab) .Icon(FEditorStyle::GetBrush("DataTableEditor.Tabs.Properties")) .Label(LOCTEXT("DataTableDetails", "Data Table Details")) .TabColorScale(GetTabColorScale()) [ SNew(SBorder) .Padding(2) .BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder")) [ PropertyView.ToSharedRef() ] ]; } void FDataTableEditor::SetHighlightedRow(FName Name) { if (Name == HighlightedRowName) { return; } if (Name.IsNone()) { HighlightedRowName = NAME_None; // Synchronize the list views RowNamesListView->ClearSelection(); CellsListView->ClearSelection(); } else { HighlightedRowName = Name; FDataTableEditorRowListViewDataPtr* NewSelectionPtr = VisibleRows.FindByPredicate([&Name](const FDataTableEditorRowListViewDataPtr& RowData) -> bool { return RowData->RowId == Name; }); // Synchronize the list views if (NewSelectionPtr) { RowNamesListView->SetSelection(*NewSelectionPtr); CellsListView->SetSelection(*NewSelectionPtr); CellsListView->RequestScrollIntoView(*NewSelectionPtr); } else { RowNamesListView->ClearSelection(); CellsListView->ClearSelection(); } } } #undef LOCTEXT_NAMESPACE