// Copyright Epic Games, Inc. All Rights Reserved. #include "ChooserTableEditor.h" #include "Chooser.h" #include "ChooserDetails.h" #include "ChooserEditorWidgets.h" #include "ChooserFindProperties.h" #include "ChooserTableEditorCommands.h" #include "ClassViewerFilter.h" #include "DetailCategoryBuilder.h" #include "Factories.h" #include "GraphEditorSettings.h" #include "IDetailsView.h" #include "IPropertyAccessEditor.h" #include "LandscapeRender.h" #include "ObjectChooserClassFilter.h" #include "ObjectChooserWidgetFactories.h" #include "ObjectChooser_Asset.h" #include "ObjectChooser_Class.h" #include "PersonaModule.h" #include "StructUtils/PropertyBag.h" #include "PropertyCustomizationHelpers.h" #include "PropertyEditorModule.h" #include "RandomizeColumn.h" #include "SAssetDropTarget.h" #include "SChooserColumnHandle.h" #include "SClassViewer.h" #include "ScopedTransaction.h" #include "SNestedChooserTree.h" #include "SourceCodeNavigation.h" #include "StructViewerModule.h" #include "DragAndDrop/DecoratedDragDropOp.h" #include "Framework/Docking/TabManager.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Misc/StringOutputDevice.h" #include "Misc/TransactionObjectEvent.h" #include "Modules/ModuleManager.h" #include "Styling/AppStyle.h" #include "SChooserTableRow.h" #include "Elements/Framework/TypedElementQueryBuilder.h" #include "Widgets/Docking/SDockTab.h" #include "Widgets/Input/SComboButton.h" #include "Widgets/Input/SHyperlink.h" #include "Widgets/Input/SNumericEntryBox.h" #include "Widgets/Layout/SScrollBox.h" #include "Widgets/Layout/SWidgetSwitcher.h" #include "Widgets/Views/SListView.h" #include "ToolMenus.h" #include "UnrealExporter.h" #include "Exporters/Exporter.h" #include "HAL/PlatformApplicationMisc.h" #include "Framework/Commands/GenericCommands.h" #include "SPositiveActionButton.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(ChooserTableEditor) #define LOCTEXT_NAMESPACE "ChooserEditor" namespace UE::ChooserEditor { const FName FChooserTableEditor::ToolkitFName( TEXT( "ChooserTableEditor" ) ); const FName FChooserTableEditor::PropertiesTabId( TEXT( "ChooserEditor_Properties" ) ); const FName FChooserTableEditor::FindReplaceTabId( TEXT( "ChooserEditor_FindReplace" ) ); const FName FChooserTableEditor::TableTabId( TEXT( "ChooserEditor_Table" ) ); const FName FChooserTableEditor::NestedTablesTreeTabId( TEXT( "ChooserEditor_NestedTables" ) ); constexpr int HistorySize = 16; void FChooserTableEditor::AddHistory() { // remove anything ahead of this in the history, if we had gone back while (HistoryIndex !=0) { History.PopFront(); HistoryIndex--; } if (History.Num() >= HistorySize) { History.Pop(); } History.AddFront(GetChooser()); } bool FChooserTableEditor::CanNavigateBack() const { return HistoryIndex < History.Num() - 1; } void FChooserTableEditor::NavigateBack() { if (HistoryIndex < History.Num() - 1) { HistoryIndex++; SetChooserTableToEdit(History[HistoryIndex], false); } } bool FChooserTableEditor::CanNavigateForward() const { return HistoryIndex > 0; } void FChooserTableEditor::NavigateForward() { if (HistoryIndex > 0) { HistoryIndex--; SetChooserTableToEdit(History[HistoryIndex], false); } } void FChooserTableEditor::SetChooserTableToEdit(UChooserTable* Chooser, bool bApplyToHistory) { if (Chooser == GetChooser()) { return; } BreadcrumbTrail->ClearCrumbs(); TArray OuterList; OuterList.Push(Chooser); while(OuterList.Last() != GetRootChooser()) { OuterList.Push(Cast(OuterList.Last()->GetOuter())); } while(!OuterList.IsEmpty()) { UChooserTable* Popped = OuterList.Pop(); BreadcrumbTrail->PushCrumb(FText::FromString(Popped->GetName()), Popped); } if (bApplyToHistory) { AddHistory(); } RefreshAll(); } void FChooserTableEditor::PushChooserTableToEdit(UChooserTable* Chooser) { BreadcrumbTrail->PushCrumb(FText::FromString(Chooser->GetName()), Chooser); AddHistory(); RefreshAll(); } void FChooserTableEditor::PopChooserTableToEdit() { if (BreadcrumbTrail->HasCrumbs()) { BreadcrumbTrail->PopCrumb(); RefreshAll(); } } void FChooserTableEditor::RegisterTabSpawners(const TSharedRef& InTabManager) { WorkspaceMenuCategory = InTabManager->AddLocalWorkspaceMenuCategory(LOCTEXT("WorkspaceMenu_ChooserTableEditor", "Chooser Table Editor")); FAssetEditorToolkit::RegisterTabSpawners(InTabManager); InTabManager->RegisterTabSpawner( PropertiesTabId, FOnSpawnTab::CreateSP(this, &FChooserTableEditor::SpawnPropertiesTab) ) .SetDisplayName( LOCTEXT("PropertiesTab", "Details") ) .SetGroup(WorkspaceMenuCategory.ToSharedRef()) .SetIcon(FSlateIcon("EditorStyle", "LevelEditor.Tabs.Details")); InTabManager->RegisterTabSpawner( TableTabId, FOnSpawnTab::CreateSP(this, &FChooserTableEditor::SpawnTableTab) ) .SetDisplayName( LOCTEXT("TableTab", "Chooser Table") ) .SetGroup(WorkspaceMenuCategory.ToSharedRef()) .SetIcon(FSlateIcon("ChooserEditorStyle", "ChooserEditor.ChooserTableIconSmall")); InTabManager->RegisterTabSpawner( NestedTablesTreeTabId, FOnSpawnTab::CreateSP(this, &FChooserTableEditor::SpawnNestedTablesTreeTab) ) .SetDisplayName( LOCTEXT("NestedTablesTab", "Nested Choosers") ) .SetGroup(WorkspaceMenuCategory.ToSharedRef()) .SetIcon(FSlateIcon("ChooserEditorStyle", "ChooserEditor.ChooserTableIconSmall")); InTabManager->RegisterTabSpawner( FindReplaceTabId, FOnSpawnTab::CreateSP(this, &FChooserTableEditor::SpawnFindReplaceTab) ) .SetDisplayName( LOCTEXT("FindReplaceTab", "Find/Replace") ) .SetGroup(WorkspaceMenuCategory.ToSharedRef()) .SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Find")); } void FChooserTableEditor::UnregisterTabSpawners(const TSharedRef& InTabManager) { FAssetEditorToolkit::UnregisterTabSpawners(InTabManager); InTabManager->UnregisterTabSpawner( TableTabId ); InTabManager->UnregisterTabSpawner( PropertiesTabId ); InTabManager->UnregisterTabSpawner( FindReplaceTabId ); } const FName FChooserTableEditor::ChooserEditorAppIdentifier( TEXT( "ChooserEditorApp" ) ); FChooserTableEditor::~FChooserTableEditor() { FTSTicker::RemoveTicker(TickDelegateHandle); FCoreUObjectDelegates::OnObjectsReplaced.RemoveAll(this); FCoreUObjectDelegates::OnObjectTransacted.RemoveAll(this); DetailsView.Reset(); } FName FChooserTableEditor::EditorName = "ChooserTableEditor"; FName FChooserTableEditor::ContextMenuName("ChooserEditorContextMenu"); // todo: for this to actually be extensible this needs to be somewhere public FName FChooserTableEditor::GetEditorName() const { return EditorName; } void FChooserTableEditor::MakeDebugTargetMenu(UToolMenu* InToolMenu) { static FName SectionName = "Select Debug Target"; InToolMenu->bSearchable = true; InToolMenu->AddMenuEntry( SectionName, FToolMenuEntry::InitMenuEntry( "None", LOCTEXT("None", "None"), LOCTEXT("None Tooltip", "Clear selected debug target"), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([this]() { UChooserTable* Chooser = GetRootChooser(); Chooser->ResetDebugTarget(); if (Chooser->GetEnableDebugTesting()) { Chooser->SetEnableDebugTesting(false); Chooser->SetDebugTestValuesValid(false); UpdateTableColumns(); } }), FCanExecuteAction() ) )); InToolMenu->AddMenuEntry( SectionName, FToolMenuEntry::InitMenuEntry( "Manual", LOCTEXT("Manual Testing", "Manual Testing"), LOCTEXT("Manual Tooltip", "Test the chooser by manually entering values for each column"), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([this]() { UChooserTable* Chooser = GetRootChooser(); Chooser->ResetDebugTarget(); if (!Chooser->GetEnableDebugTesting()) { Chooser->SetEnableDebugTesting(true); Chooser->SetDebugTestValuesValid(true); UpdateTableColumns(); } }), FCanExecuteAction() ) )); const UChooserTable* Chooser = GetChooser(); Chooser->IterateRecentContextObjects([this, InToolMenu](const FString& ObjectName) { InToolMenu->AddMenuEntry( SectionName, FToolMenuEntry::InitMenuEntry( FName(ObjectName), FText::FromString(ObjectName), LOCTEXT("Select Object ToolTip", "Select this object as the debug target"), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([this, ObjectName]() { UChooserTable* Chooser = GetRootChooser(); Chooser->SetDebugTarget(ObjectName); Chooser->SetDebugTestValuesValid(false); if (!Chooser->GetEnableDebugTesting()) { Chooser->SetEnableDebugTesting(true); UpdateTableColumns(); } }), FCanExecuteAction() ) )); } ); } TSharedPtr FChooserTableEditor::GenerateRowContextMenu() { UToolMenus* ToolMenus = UToolMenus::Get(); FToolMenuContext ToolMenuContext; InitToolMenuContext(ToolMenuContext); return ToolMenus->GenerateWidget(ContextMenuName, ToolMenuContext); } void FChooserTableEditor::RegisterToolbar() { UToolMenus* ToolMenus = UToolMenus::Get(); UToolMenu* ToolBar; FName ParentName; const FName MenuName = GetToolMenuToolbarName(ParentName); if (ToolMenus->IsMenuRegistered(MenuName)) { ToolBar = ToolMenus->ExtendMenu(MenuName); } else { ToolBar = UToolMenus::Get()->RegisterMenu(MenuName, ParentName, EMultiBoxType::ToolBar); } const FChooserTableEditorCommands& Commands = FChooserTableEditorCommands::Get(); FToolMenuInsert InsertAfterAssetSection("Asset", EToolMenuInsertType::After); { FToolMenuSection& Section = ToolBar->AddSection("Chooser", TAttribute(), InsertAfterAssetSection); Section.AddEntry(FToolMenuEntry::InitToolBarButton( Commands.EditChooserSettings, TAttribute(), TAttribute(), FSlateIcon("EditorStyle", "FullBlueprintEditor.EditGlobalOptions"))); Section.AddEntry(FToolMenuEntry::InitToolBarButton(Commands.AutoPopulateAll)); Section.AddDynamicEntry("DebuggingCommands", FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection) { UChooserEditorToolMenuContext* Context = InSection.FindContext(); if (Context) { if (TSharedPtr ChooserEditor = Context->ChooserEditor.Pin()) { InSection.AddEntry(FToolMenuEntry::InitComboButton( "SelectDebugTarget", FToolUIActionChoice(), FNewToolMenuDelegate::CreateSP(ChooserEditor.Get(), &FChooserTableEditor::MakeDebugTargetMenu), TAttribute::CreateLambda([Chooser = ChooserEditor->GetRootChooser() ] { if (Chooser->HasDebugTarget()) { return FText::FromString(Chooser->GetDebugTargetName()); } else { return Chooser->GetEnableDebugTesting() ? LOCTEXT("Manual Testing", "Manual Testing") : LOCTEXT("Debug Target", "Debug Target"); } }), LOCTEXT("Debug Target Tooltip", "Select an object that has recently been the context object for this chooser to visualize the selection results"))); } } })); } } void FChooserTableEditor::RegisterMenus() { UToolMenus* ToolMenus = UToolMenus::Get(); const FChooserTableEditorCommands& Commands = FChooserTableEditorCommands::Get(); // Table Context Menu UToolMenu* ToolMenu; if (ToolMenus->IsMenuRegistered(ContextMenuName)) { ToolMenu = ToolMenus->ExtendMenu(ContextMenuName); } else { ToolMenu = UToolMenus::Get()->RegisterMenu(ContextMenuName, NAME_None, EMultiBoxType::Menu); } if (ToolMenu) { FToolMenuSection& Section = ToolMenu->AddSection("ChooserEditorContext", TAttribute()); Section.AddEntry(FToolMenuEntry::InitMenuEntry(FGenericCommands::Get().Copy)); Section.AddEntry(FToolMenuEntry::InitMenuEntry(FGenericCommands::Get().Cut)); Section.AddEntry(FToolMenuEntry::InitMenuEntry(FGenericCommands::Get().Paste)); Section.AddEntry(FToolMenuEntry::InitMenuEntry(FGenericCommands::Get().Duplicate)); Section.AddEntry(FToolMenuEntry::InitMenuEntry(FGenericCommands::Get().Delete)); Section.AddEntry(FToolMenuEntry::InitMenuEntry(Commands.Disable)); Section.AddEntry(FToolMenuEntry::InitMenuEntry(Commands.MoveUp)); Section.AddEntry(FToolMenuEntry::InitMenuEntry(Commands.MoveDown)); Section.AddEntry(FToolMenuEntry::InitMenuEntry(Commands.MoveLeft)); Section.AddEntry(FToolMenuEntry::InitMenuEntry(Commands.MoveRight)); Section.AddEntry(FToolMenuEntry::InitMenuEntry(Commands.AutoPopulateSelection)); Section.AddDynamicEntry("ColumnInputType", FNewToolMenuSectionDelegate::CreateLambda([this] (FToolMenuSection& Section) { if (CurrentSelectionType == ESelectionType::Column && SelectedColumn) { Section.AddSubMenu("ParameterType", LOCTEXT("Parameter Type", "Parameter Type"), LOCTEXT("Parameter Type Tooltip", "Change the type of input/output parameter for this column"), FNewToolMenuChoice(FOnGetContent::CreateLambda([this]() { int ColumnIndex = SelectedColumn->Column; UChooserTable* Chooser = SelectedColumn->Chooser; FStructViewerInitializationOptions Options; Options.StructFilter = MakeShared(Chooser->ColumnsStructs[ColumnIndex].Get().GetInputBaseType()); Options.bAllowViewOptions = false; Options.bShowNoneOption = false; Options.NameTypeToDisplay = EStructViewerNameTypeToDisplay::DisplayName; // Add class filter for columns here TSharedRef Widget = FModuleManager::LoadModuleChecked("StructViewer").CreateStructViewer(Options, FOnStructPicked::CreateLambda([this, ColumnIndex](const UScriptStruct* ChosenStruct) { const FScopedTransaction Transaction(LOCTEXT("SetColumnInputType", "Set Column Input Type")); UChooserTable* ChooserTable = GetChooser(); ChooserTable->ColumnsStructs[ColumnIndex].GetMutable().SetInputType(ChosenStruct); ChooserTable->Modify(true); UpdateTableColumns(); UpdateTableRows(); if (SelectedColumn && SelectedColumn->Column == ColumnIndex) { // if this column was selected, reselect to refresh the details widgets SelectColumn(ChooserTable, ColumnIndex); } })); return Widget; })) ); } })); } struct Local { static void FillEditMenu(FMenuBuilder& MenuBuilder) { MenuBuilder.BeginSection("ChooserEditing", LOCTEXT("Chooser Table Editing", "Chooser Table")); { MenuBuilder.AddMenuEntry(FGenericCommands::Get().Copy, NAME_None); MenuBuilder.AddMenuEntry(FGenericCommands::Get().Cut, NAME_None); MenuBuilder.AddMenuEntry(FGenericCommands::Get().Paste, NAME_None); MenuBuilder.AddMenuEntry(FGenericCommands::Get().Duplicate, NAME_None, LOCTEXT("Duplicate Selection", "Duplicate Selection")); MenuBuilder.AddMenuEntry(FGenericCommands::Get().Delete, NAME_None, LOCTEXT("Delete Selection", "Delete Selection")); MenuBuilder.AddMenuEntry(FChooserTableEditorCommands::Get().Disable, NAME_None, LOCTEXT("Disable Selection", "Disable Selection")); MenuBuilder.AddMenuEntry(FChooserTableEditorCommands::Get().RemoveDisabledData, NAME_None); } MenuBuilder.EndSection(); } }; TSharedPtr MenuExtender = MakeShareable(new FExtender); // Extend the Edit menu MenuExtender->AddMenuExtension( "EditHistory", EExtensionHook::After, GetToolkitCommands(), FMenuExtensionDelegate::CreateStatic(&Local::FillEditMenu)); AddMenuExtender(MenuExtender); } void FChooserTableEditor::InitToolMenuContext(FToolMenuContext& MenuContext) { FAssetEditorToolkit::InitToolMenuContext(MenuContext); UChooserEditorToolMenuContext* Context = NewObject(); Context->ChooserEditor = SharedThis(this); MenuContext.AppendCommandList(GetToolkitCommands()); MenuContext.AddObject(Context); } void FChooserTableEditor::SaveAsset_Execute() { AutoPopulateAll(); FAssetEditorToolkit::SaveAsset_Execute(); } void FChooserTableEditor::BindCommands() { const FChooserTableEditorCommands& Commands = FChooserTableEditorCommands::Get(); ToolkitCommands->MapAction( Commands.EditChooserSettings, FExecuteAction::CreateSP(this, &FChooserTableEditor::SelectRootProperties)); ToolkitCommands->MapAction( Commands.AutoPopulateAll, FExecuteAction::CreateSP(this, &FChooserTableEditor::AutoPopulateAll)); ToolkitCommands->MapAction( Commands.RemoveDisabledData, FExecuteAction::CreateSP(this, &FChooserTableEditor::RemoveDisabledData)); ToolkitCommands->MapAction( FGenericCommands::Get().Delete, FExecuteAction::CreateSP(this, &FChooserTableEditor::DeleteSelection), FCanExecuteAction::CreateSP(this, &FChooserTableEditor::HasSelection) ); ToolkitCommands->MapAction( FGenericCommands::Get().Duplicate, FExecuteAction::CreateSP(this, &FChooserTableEditor::DuplicateSelection), FCanExecuteAction::CreateSP(this, &FChooserTableEditor::HasSelection) ); ToolkitCommands->MapAction( Commands.AutoPopulateSelection, FExecuteAction::CreateSP(this, &FChooserTableEditor::AutoPopulateSelection), FCanExecuteAction::CreateSP(this, &FChooserTableEditor::CanAutoPopulateSelection), FGetActionCheckState(),FIsActionButtonVisible::CreateSP(this, &FChooserTableEditor::HasSelection) ); ToolkitCommands->MapAction( Commands.Disable, FExecuteAction::CreateSP(this, &FChooserTableEditor::ToggleDisableSelection), FCanExecuteAction::CreateSP(this, &FChooserTableEditor::HasSelection), FIsActionChecked::CreateSP(this, &FChooserTableEditor::IsSelectionDisabled), FIsActionButtonVisible::CreateSP(this, &FChooserTableEditor::HasSelection) ); ToolkitCommands->MapAction( FGenericCommands::Get().Copy, FExecuteAction::CreateSP(this, &FChooserTableEditor::CopySelection), FCanExecuteAction::CreateSP(this, &FChooserTableEditor::HasSelection)); ToolkitCommands->MapAction( FGenericCommands::Get().Cut, FExecuteAction::CreateSP(this, &FChooserTableEditor::CutSelection), FCanExecuteAction::CreateSP(this, &FChooserTableEditor::HasSelection)); ToolkitCommands->MapAction( FGenericCommands::Get().Paste, FExecuteAction::CreateSP(this, &FChooserTableEditor::Paste), FCanExecuteAction::CreateSP(this, &FChooserTableEditor::CanPaste)); ToolkitCommands->MapAction( Commands.MoveUp, FExecuteAction::CreateSP(this, &FChooserTableEditor::MoveRowsUp), FCanExecuteAction::CreateSP(this, &FChooserTableEditor::CanMoveRowsUp), FIsActionChecked(), FIsActionButtonVisible::CreateSP(this, &FChooserTableEditor::HasRowsSelected) ); ToolkitCommands->MapAction( Commands.MoveDown, FExecuteAction::CreateSP(this, &FChooserTableEditor::MoveRowsDown), FCanExecuteAction::CreateSP(this, &FChooserTableEditor::CanMoveRowsDown), FIsActionChecked(), FIsActionButtonVisible::CreateSP(this, &FChooserTableEditor::HasRowsSelected) ); ToolkitCommands->MapAction( Commands.MoveLeft, FExecuteAction::CreateSP(this, &FChooserTableEditor::MoveColumnLeft), FCanExecuteAction::CreateSP(this, &FChooserTableEditor::CanMoveColumnLeft), FIsActionChecked(), FIsActionButtonVisible::CreateSP(this, &FChooserTableEditor::HasColumnSelected) ); ToolkitCommands->MapAction( Commands.MoveRight, FExecuteAction::CreateSP(this, &FChooserTableEditor::MoveColumnRight), FCanExecuteAction::CreateSP(this, &FChooserTableEditor::CanMoveColumnRight), FIsActionChecked(), FIsActionButtonVisible::CreateSP(this, &FChooserTableEditor::HasColumnSelected) ); } void FChooserTableEditor::OnObjectsTransacted(UObject* Object, const FTransactionObjectEvent& Event) { if (UChooserTable* ChooserTable = Cast(Object)) { // if this is the chooser we're editing if (GetChooser() == ChooserTable) { if (CurrentSelectionType == ESelectionType::Rows) { // refresh details if we have rows selected RefreshRowSelectionDetails(); } } } if (UChooserRowDetails* RowDetails = Cast(Object)) { // if this is for the chooser we're editing if (GetChooser() == RowDetails->Chooser) { if (RowDetails->Chooser->ResultsStructs.IsValidIndex(RowDetails->Row)) { // copy all the values over TValueOrError Result = RowDetails->Properties.GetValueStruct("Result", FInstancedStruct::StaticStruct()); if (Result.IsValid()) { RowDetails->Chooser->ResultsStructs[RowDetails->Row] = Result.GetValue().Get(); } int ColumnIndex = 0; for (FInstancedStruct& ColumnData : RowDetails->Chooser->ColumnsStructs) { FChooserColumnBase& Column = ColumnData.GetMutable(); Column.SetFromDetails(RowDetails->Properties, ColumnIndex, RowDetails->Row); ColumnIndex++; } TValueOrError DisabledResult = RowDetails->Properties.GetValueBool("Disabled"); RowDetails->Chooser->DisabledRows[RowDetails->Row] = DisabledResult.GetValue(); } else if (RowDetails->Row == ColumnWidget_SpecialIndex_Fallback) { TValueOrError Result = RowDetails->Properties.GetValueStruct("Result", FInstancedStruct::StaticStruct()); if (Result.IsValid()) { RowDetails->Chooser->FallbackResult = Result.GetValue().Get(); } int ColumnIndex = 0; for (FInstancedStruct& ColumnData : RowDetails->Chooser->ColumnsStructs) { FChooserColumnBase& Column = ColumnData.GetMutable(); Column.SetFromDetails(RowDetails->Properties, ColumnIndex, RowDetails->Row); ColumnIndex++; } } QueueRefreshAll(); } } } bool FChooserTableEditor::HandleTicker(float DeltaTime) { if (bQueueRefreshAll) { RefreshAll(); bQueueRefreshAll = false; } return true; } void FChooserTableEditor::InitEditor( const EToolkitMode::Type Mode, const TSharedPtr< class IToolkitHost >& InitToolkitHost, const TArray& ObjectsToEdit, FGetDetailsViewObjects GetDetailsViewObjects ) { TickDelegateHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateRaw(this, &FChooserTableEditor::HandleTicker), 1.f); UChooserTable* Chooser = Cast(ObjectsToEdit[0]); RootChooser = Chooser->GetRootChooser(); check(RootChooser); History.Reserve(HistorySize); BreadcrumbTrail = SNew(SBreadcrumbTrail) .ButtonStyle(FAppStyle::Get(), "GraphBreadcrumbButton") .TextStyle(FAppStyle::Get(), "GraphBreadcrumbButtonText") .ButtonContentPadding( FMargin(4.f, 2.f) ) .DelimiterImage( FAppStyle::GetBrush("BreadcrumbTrail.Delimiter") ) .OnCrumbPushed_Lambda([this](UChooserTable* Table) { RefreshAll(); }) .OnCrumbClicked_Lambda([this](UChooserTable* Table) { AddHistory(); RefreshAll(); }) .GetCrumbMenuContent_Lambda([this](UChooserTable* Item) { return MakeChoosersMenu(Item); }) ; BreadcrumbTrail->PushCrumb(FText::FromString(RootChooser->GetName()), RootChooser); AddHistory(); FCoreUObjectDelegates::OnObjectsReplaced.AddSP(this, &FChooserTableEditor::OnObjectsReplaced); FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked( "PropertyEditor" ); FDetailsViewArgs DetailsViewArgs; DetailsViewArgs.NotifyHook = this; DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea; DetailsView = PropertyEditorModule.CreateDetailView( DetailsViewArgs ); const TSharedRef StandaloneDefaultLayout = FTabManager::NewLayout( "Standalone_ChooserTableEditor_Layout_v1.6" ) ->AddArea ( FTabManager::NewPrimaryArea() ->SetOrientation(Orient_Vertical) ->Split ( FTabManager::NewSplitter() ->Split ( FTabManager::NewStack() ->SetSizeCoefficient(0.7) ->AddTab( TableTabId, ETabState::OpenedTab ) ) ->Split ( FTabManager::NewSplitter()->SetOrientation(Orient_Vertical) ->SetSizeCoefficient(0.3) ->Split ( FTabManager::NewStack() ->SetSizeCoefficient(0.5) ->AddTab( PropertiesTabId, ETabState::OpenedTab) ) ->Split ( FTabManager::NewStack() ->SetSizeCoefficient(0.5) ->AddTab( NestedTablesTreeTabId, ETabState::OpenedTab ) ) ) ) ); const bool bCreateDefaultStandaloneMenu = true; const bool bCreateDefaultToolbar = true; FAssetEditorToolkit::InitAssetEditor( Mode, InitToolkitHost, FChooserTableEditor::ChooserEditorAppIdentifier, StandaloneDefaultLayout, bCreateDefaultStandaloneMenu, bCreateDefaultToolbar, ObjectsToEdit ); BindCommands(); // todo: should these be triggered once per session by the module? RegisterToolbar(); RegisterMenus(); RegenerateMenusAndToolbars(); SelectRootProperties(); SetChooserTableToEdit(Chooser); FAnimAssetFindReplaceConfig FindReplaceConfig; FindReplaceConfig.InitialProcessorClass = UChooserFindProperties::StaticClass(); FCoreUObjectDelegates::OnObjectTransacted.AddSP(this, &FChooserTableEditor::OnObjectsTransacted); } void FChooserTableEditor::FocusWindow(UObject* ObjectToFocusOn) { if (UChooserTable* Chooser = Cast(ObjectToFocusOn)) { SetChooserTableToEdit(Chooser); } // refresh, even if we set the same chooser we were already editing. (Rewind Debugger double click enables debug testing, which requires recreating the header widgets) RefreshAll(); FAssetEditorToolkit::FocusWindow(ObjectToFocusOn); } FName FChooserTableEditor::GetToolkitFName() const { return ToolkitFName; } FText FChooserTableEditor::GetBaseToolkitName() const { return LOCTEXT("AppLabel", "Chooser Table Editor"); } void FChooserTableEditor::RefreshAll() { if (HeaderRow) { // Cache Selection state ESelectionType CachedSelectionType = CurrentSelectionType; int SelectedColumnIndex = -1; UChooserTable* SelectedChooser = nullptr; TArray CachedSelectedRows; if (CachedSelectionType == ESelectionType::Column) { SelectedColumnIndex = SelectedColumn->Column; SelectedChooser = SelectedColumn->Chooser; } else if (CachedSelectionType == ESelectionType::Rows) { if (!SelectedRows.IsEmpty()) { SelectedChooser = SelectedRows[0]->Chooser; } for(const TObjectPtr& SelectedRow : SelectedRows) { CachedSelectedRows.Add(SelectedRow->Row); } } UpdateTableColumns(); UpdateTableRows(); // reapply cached selection state if (CachedSelectionType == ESelectionType::Root) { SelectRootProperties(); } else if (CachedSelectionType == ESelectionType::Column) { SelectColumn(SelectedChooser, SelectedColumnIndex); } else if (CachedSelectionType == ESelectionType::Rows) { SelectRows(CachedSelectedRows); } } RefreshNestedObjectTree(); } void FChooserTableEditor::QueueRefreshAll() { bQueueRefreshAll = true; } void FChooserTableEditor::RefreshNestedObjectTree() { if (NestedChooserTree.IsValid()) { NestedChooserTree->RefreshAll(); } } bool FChooserTableEditor::MatchesContext(const FTransactionContext& InContext, const TArray>& TransactionObjectContexts) const { TArray ContainedObjects; GetObjectsWithOuter(RootChooser->GetPackage(), ContainedObjects, true); for(const TPair& Entry : TransactionObjectContexts) { if (ContainedObjects.Contains(Entry.Key)) { return true; } } return false; } void FChooserTableEditor::PostUndo(bool bSuccess) { RefreshAll(); } void FChooserTableEditor::PostRedo(bool bSuccess) { RefreshAll(); } void FChooserTableEditor::NotifyPreChange(FProperty* PropertyAboutToChange) { } void FChooserTableEditor::NotifyPostChange(const FPropertyChangedEvent& PropertyChangedEvent, FProperty* PropertyThatChanged) { // Called on details panel edits if (CurrentSelectionType==ESelectionType::Root) { // Editing the root in the details panel can change ContextData that means all wigets need to be refreshed UpdateTableColumns(); UpdateTableRows(); SelectRootProperties(); } if (CurrentSelectionType==ESelectionType::Column) { check(SelectedColumn); int SelectedColumnIndex = SelectedColumn->Column; UChooserTable* SelectedColumnChooser = SelectedColumn->Chooser; // Editing column properties can change the column type, which requires refreshing everything UpdateTableColumns(); UpdateTableRows(); SelectColumn(SelectedColumnChooser, SelectedColumnIndex); } // editing row data should not require any refreshing } FText FChooserTableEditor::GetToolkitName() const { check( RootChooser ); return FText::FromString(RootChooser->GetName()); } FText FChooserTableEditor::GetToolkitToolTipText() const { check( RootChooser ); return FAssetEditorToolkit::GetToolTipTextForObject(RootChooser); } FLinearColor FChooserTableEditor::GetWorldCentricTabColorScale() const { return FLinearColor( 0.5f, 0.0f, 0.0f, 0.5f ); } void FChooserTableEditor::SetPropertyVisibilityDelegate(FIsPropertyVisible InVisibilityDelegate) { DetailsView->SetIsPropertyVisibleDelegate(InVisibilityDelegate); DetailsView->ForceRefresh(); } void FChooserTableEditor::SetPropertyEditingEnabledDelegate(FIsPropertyEditingEnabled InPropertyEditingDelegate) { DetailsView->SetIsPropertyEditingEnabledDelegate(InPropertyEditingDelegate); DetailsView->ForceRefresh(); } TSharedRef FChooserTableEditor::SpawnPropertiesTab( const FSpawnTabArgs& Args ) { check( Args.GetTabId() == PropertiesTabId ); return SNew(SDockTab) .Label( LOCTEXT("GenericDetailsTitle", "Details") ) .TabColorScale( GetTabColorScale() ) .OnCanCloseTab_Lambda([]() { return false; }) [ DetailsView.ToSharedRef() ]; } TSharedRef FChooserTableEditor::SpawnFindReplaceTab( const FSpawnTabArgs& Args ) { check( Args.GetTabId() == FindReplaceTabId ); FPersonaModule& PersonaModule = FModuleManager::LoadModuleChecked("Persona"); FAnimAssetFindReplaceConfig Config; Config.InitialProcessorClass = UChooserFindProperties::StaticClass(); return SNew(SDockTab) .Label( LOCTEXT("FindReplaceTitle", "Find/Replace") ) .TabColorScale( GetTabColorScale() ) [ PersonaModule.CreateFindReplaceWidget(Config) ]; } TSharedRef FChooserTableEditor::GenerateTableRow(TSharedPtr InItem, const TSharedRef& OwnerTable) { UChooserTable* Chooser = GetChooser(); return SNew(SChooserTableRow, OwnerTable) .Entry(InItem).Chooser(Chooser).Editor(this); } void FChooserTableEditor::SelectRootProperties() { if( DetailsView.IsValid() ) { // point the details view to the main table DetailsView->SetObject( GetRootChooser() ); CurrentSelectionType = ESelectionType::Root; } } void FChooserTableEditor::RemoveDisabledData() { UChooserTable* Chooser = GetChooser(); const FScopedTransaction Transaction(LOCTEXT("Remove Disabled Data", "Remove Disabled Data")); Chooser->Modify(true); Chooser->RemoveDisabledData(); RefreshAll(); } int FChooserTableEditor::MoveColumn(int SourceIndex, int TargetIndex) { UChooserTable* Chooser = GetChooser(); TargetIndex = FMath::Clamp(TargetIndex, 0, Chooser->ColumnsStructs.Num()); if (SourceIndex < 0 || SourceIndex == TargetIndex) { return TargetIndex; } const FScopedTransaction Transaction(LOCTEXT("Move Row", "Move Row")); Chooser->Modify(true); FInstancedStruct ColumnData = Chooser->ColumnsStructs[SourceIndex]; Chooser->ColumnsStructs.RemoveAt(SourceIndex); if (SourceIndex < TargetIndex) { TargetIndex--; } if (TargetIndex == Chooser->ColumnsStructs.Num()) { if (Chooser->ColumnsStructs.Last().GetPtr()) { // never drop after a Randomize Column; TargetIndex--; } } Chooser->ColumnsStructs.Insert(ColumnData, TargetIndex); RefreshAll(); return TargetIndex; } int FChooserTableEditor::MoveRow(int SourceRowIndex, int TargetRowIndex) { UChooserTable* Chooser = GetChooser(); TargetRowIndex = FMath::Min(TargetRowIndex,Chooser->ResultsStructs.Num()); const FScopedTransaction Transaction(LOCTEXT("Move Row", "Move Row")); Chooser->Modify(true); for (FInstancedStruct& ColStruct : Chooser->ColumnsStructs) { FChooserColumnBase& Column = ColStruct.GetMutable(); Column.MoveRow(SourceRowIndex, TargetRowIndex); } FInstancedStruct Result = Chooser->ResultsStructs[SourceRowIndex]; Chooser->ResultsStructs.RemoveAt(SourceRowIndex); bool bDisabled = Chooser->DisabledRows[SourceRowIndex]; Chooser->DisabledRows.RemoveAt(SourceRowIndex); if (SourceRowIndex < TargetRowIndex) { TargetRowIndex--; } Chooser->ResultsStructs.Insert(Result, TargetRowIndex); Chooser->DisabledRows.Insert(bDisabled, TargetRowIndex); UpdateTableRows(); return TargetRowIndex; } void FChooserTableEditor::SelectRow(int32 RowIndex, bool bClear) { if (TSharedPtr* Row = TableRows.FindByPredicate([RowIndex](const TSharedPtr& InRow) { return InRow->RowIndex == RowIndex; })) { if (!TableView->IsItemSelected(*Row)) { if (bClear) { TableView->ClearSelection(); } TableView->SetItemSelection(*Row, true, ESelectInfo::OnMouseClick); } } } void FChooserTableEditor::SelectRows(const TArrayView& Rows) { TableView->ClearSelection(); TArray> RowsToSelect; for (TSharedPtr& Row : TableRows) { if (Rows.Contains(Row->RowIndex)) { RowsToSelect.Add(Row); } } TableView->SetItemSelection(RowsToSelect, true); } void FChooserTableEditor::ClearSelectedRows() { SelectedRows.SetNum(0); TableView->ClearSelection(); SelectRootProperties(); } bool FChooserTableEditor::IsRowSelected(int32 RowIndex) { for(auto& SelectedRow:SelectedRows) { if (SelectedRow->Row == RowIndex) { return true; } } return false; } bool FChooserTableEditor::IsColumnSelected(int32 ColumnIndex) { return (CurrentSelectionType == ESelectionType::Column && SelectedColumn && SelectedColumn->Column == ColumnIndex); } void FChooserTableEditor::UpdateTableColumns() { UChooserTable* Chooser = GetChooser(); HeaderRow->ClearColumns(); HeaderRow->AddColumn(SHeaderRow::Column("Handles") .DefaultLabel(FText()) .ManualWidth(30)); if (Chooser->ResultType != EObjectChooserResultType::NoPrimaryResult) { HeaderRow->AddColumn(SHeaderRow::Column("Result") .ManualWidth_Lambda([Chooser]() { return Chooser->EditorResultsColumnWidth; } ) .OnWidthChanged_Lambda([Chooser](float NewWidth) { Chooser->EditorResultsColumnWidth = NewWidth; }) .HeaderContent() [ SNew(SVerticalBox) + SVerticalBox::Slot() .VAlign(VAlign_Top) [ SNew(STextBlock) .Text(LOCTEXT("Result", "Result")) .ToolTipText(LOCTEXT("ResultTooltip", "The Result is the asset which will be returned if a row is selected (or other Chooser to evaluate to get the asset to return")) ] ]); } FName ColumnId("ChooserColumn", 1); int NumColumns = Chooser->ColumnsStructs.Num(); for(int ColumnIndex = 0; ColumnIndex < NumColumns; ColumnIndex++) { FChooserColumnBase& Column = Chooser->ColumnsStructs[ColumnIndex].GetMutable(); TSharedPtr HeaderWidget = FObjectChooserWidgetFactories::CreateColumnWidget(&Column, Chooser->ColumnsStructs[ColumnIndex].GetScriptStruct(), Chooser->GetRootChooser(), -1); if (!HeaderWidget.IsValid()) { HeaderWidget = SNullWidget::NullWidget; } HeaderRow->AddColumn(SHeaderRow::FColumn::FArguments() .ColumnId(ColumnId) .ManualWidth(Column.EditorColumnWidth) .ManualWidth_Lambda([&Column]() { return Column.EditorColumnWidth; } ) .OnWidthChanged_Lambda([&Column](float NewWidth) { Column.EditorColumnWidth = NewWidth; }) .HeaderComboVisibility(EHeaderComboVisibility::Ghosted) .HeaderContent() [ SNew(SChooserColumnHandle) .ChooserEditor(this) .ColumnIndex(ColumnIndex) .NoDropAfter(Chooser->ColumnsStructs[ColumnIndex].GetPtr() != nullptr) [ HeaderWidget.ToSharedRef() ] ]); ColumnId.SetNumber(ColumnId.GetNumber() + 1); } HeaderRow->AddColumn( SHeaderRow::FColumn::FArguments() .ColumnId("Add") .FillWidth(1.0) .HeaderContent( ) [ SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight() [ SNew(SHorizontalBox) + SHorizontalBox::Slot().MaxWidth(150) [ CreateColumnComboButton.ToSharedRef() ] ] ] ); } void FChooserTableEditor::AddColumn(const UScriptStruct* ColumnType) { FSlateApplication::Get().DismissAllMenus(); UChooserTable* Chooser = GetChooser(); const FScopedTransaction Transaction(LOCTEXT("Add Column Transaction", "Add Column")); Chooser->Modify(true); FInstancedStruct NewColumn; NewColumn.InitializeAs(ColumnType); NewColumn.GetMutable().Initialize(Chooser); const FChooserColumnBase& NewColumnRef = NewColumn.Get(); int InsertIndex = 0; if (NewColumnRef.IsRandomizeColumn()) { // add randomization column at the end (do nothing if there already is one) InsertIndex = Chooser->ColumnsStructs.Num(); if (InsertIndex == 0 || !Chooser->ColumnsStructs[InsertIndex - 1].Get().IsRandomizeColumn()) { Chooser->ColumnsStructs.Add(NewColumn); } } else if (NewColumnRef.HasOutputs()) { // add output columns at the end (but before any randomization column) InsertIndex = Chooser->ColumnsStructs.Num(); if (InsertIndex > 0 && Chooser->ColumnsStructs[InsertIndex - 1].Get().IsRandomizeColumn()) { InsertIndex--; } Chooser->ColumnsStructs.Insert(NewColumn, InsertIndex); } else { // add other columns after the last non-output, non-randomization column while(InsertIndex < Chooser->ColumnsStructs.Num()) { const FChooserColumnBase& Column = Chooser->ColumnsStructs[InsertIndex].Get(); if (Column.HasOutputs() || Column.IsRandomizeColumn()) { break; } InsertIndex++; } Chooser->ColumnsStructs.Insert(NewColumn, InsertIndex); } UpdateTableColumns(); UpdateTableRows(); SelectColumn(Chooser, InsertIndex); } void FChooserTableEditor::RefreshRowSelectionDetails() { SelectedRows.SetNum(0); UChooserTable* Chooser = GetChooser(); FPropertyBagPropertyDesc ResultPropertyDesc ("Result", EPropertyBagPropertyType::Struct, FInstancedStruct::StaticStruct()); ResultPropertyDesc.MetaData.Add({"ExcludeBaseStruct",""}); ResultPropertyDesc.MetaData.Add({"BaseStruct","/Script/Chooser.ObjectChooserBase"}); // Get the list of objects to edit the details of TArray> SelectedItems = TableView->GetSelectedItems(); if (SelectedRowsCache.Num() < SelectedItems.Num()) { SelectedRowsCache.SetNum(SelectedItems.Num()); } int32 SelectedRowsCacheIndex = 0; for(TSharedPtr& SelectedItem : SelectedItems) { if (Chooser->ResultsStructs.IsValidIndex(SelectedItem->RowIndex) || SelectedItem->RowIndex == ColumnWidget_SpecialIndex_Fallback) { if (SelectedRowsCache[SelectedRowsCacheIndex] == nullptr) { SelectedRowsCache[SelectedRowsCacheIndex] = TStrongObjectPtr(NewObject(GetTransientPackage(), NAME_None, RF_Transactional | RF_Transient)); } TObjectPtr Selection = SelectedRowsCache[SelectedRowsCacheIndex].Get(); SelectedRowsCacheIndex ++; Selection->Chooser = Chooser; Selection->Row = SelectedItem->RowIndex; Selection->Properties.Reset(); if (Chooser->ResultType != EObjectChooserResultType::NoPrimaryResult) { FInstancedStruct& Result = SelectedItem->RowIndex == ColumnWidget_SpecialIndex_Fallback? Chooser->FallbackResult : Chooser->ResultsStructs[SelectedItem->RowIndex];; Selection->Properties.AddProperties({ResultPropertyDesc}); Selection->Properties.SetValueStruct("Result", FConstStructView(FInstancedStruct::StaticStruct(), reinterpret_cast(&Result))); } int ColumnIndex = 0; for (FInstancedStruct& ColumnData : Chooser->ColumnsStructs) { FChooserColumnBase& Column = ColumnData.GetMutable(); if (Column.HasOutputs() || SelectedItem->RowIndex != ColumnWidget_SpecialIndex_Fallback) { // add each column's details, but on the fallback row, only add details for Output columns Column.AddToDetails(Selection->Properties, ColumnIndex, SelectedItem->RowIndex); } ColumnIndex++; } if (Chooser->DisabledRows.IsValidIndex(SelectedItem->RowIndex)) { Selection->Properties.AddProperty("Disabled", EPropertyBagPropertyType::Bool); Selection->Properties.SetValueBool("Disabled", Chooser->DisabledRows[SelectedItem->RowIndex]); } SelectedRows.Add(Selection); } } TArray DetailsObjects; for(auto& Item : SelectedRows) { DetailsObjects.Add(Item.Get()); } if( DetailsView.IsValid() ) { // Make sure details window is pointing to our object DetailsView->SetObjects( DetailsObjects ); } } void FChooserTableEditor::MakeChoosersMenuRecursive(UObject* Outer, FMenuBuilder& MenuBuilder, const FString& Indent = "") { TArray ChildObjects; GetObjectsWithOuter(Outer, ChildObjects, false); FString SubIndent = Indent + " "; for (UObject* Object : ChildObjects) { if (UChooserTable* Chooser = Cast(Object)) { if (Chooser == RootChooser || Chooser->GetRootChooser()->NestedObjects.Contains(Chooser)) { MenuBuilder.AddMenuEntry( FText::FromString(Indent + Chooser->GetName()), LOCTEXT("Edit Chooser ToolTip", "Browse to this Nested Chooser Table"), FSlateIcon(), FUIAction(FExecuteAction::CreateLambda([this, Chooser]() { SetChooserTableToEdit(Chooser); }))); MakeChoosersMenuRecursive(Chooser, MenuBuilder, SubIndent); } } } } TSharedRef FChooserTableEditor::MakeChoosersMenu(UObject* RootObject) { FMenuBuilder MenuBuilder(true, nullptr); MakeChoosersMenuRecursive(RootObject, MenuBuilder); return MenuBuilder.MakeWidget(); } TSharedRef FChooserTableEditor::SpawnNestedTablesTreeTab( const FSpawnTabArgs& Args ) { check( Args.GetTabId() == NestedTablesTreeTabId ); return SNew(SDockTab) .Label( LOCTEXT("NestedChooserTreeTitle", "Nested Choosers") ) [ SAssignNew(NestedChooserTree, SNestedChooserTree).ChooserEditor(this) ]; } DECLARE_DELEGATE_OneParam(FCreateStructDelegate, UScriptStruct*); struct FColumnTypeInfoStruct { int SortOrder = 100; FString Category; UScriptStruct* Type; bool operator < (const FColumnTypeInfoStruct& Other) const { if (Category == Other.Category) { return Type->GetDisplayNameText().ToString() < Other.Type->GetDisplayNameText().ToString(); } else if (SortOrder == Other.SortOrder) { return Category < Other.Category; } else { return SortOrder < Other.SortOrder; } } }; TSharedRef FChooserTableEditor::MakeCreateColumnMenu() { FMenuBuilder MenuBuilder(true, nullptr); static TArray ColumnTypes; if (ColumnTypes.IsEmpty()) { UScriptStruct* BaseType = FChooserColumnBase::StaticStruct(); for (TObjectIterator StructIt; StructIt; ++StructIt) { if (*StructIt != BaseType && StructIt->IsChildOf(BaseType)) { if (!StructIt->HasMetaData("Hidden")) { FColumnTypeInfoStruct Info; Info.Type = *StructIt; Info.Category = StructIt->HasMetaData("Category") ? StructIt->GetMetaData("Category") : "Other"; if (Info.Category == "Filter") { Info.SortOrder = 1; } else if (Info.Category == "Scoring") { Info.SortOrder = 2; } else if (Info.Category == "Output") { Info.SortOrder = 3; } else if (Info.Category == "Random") { Info.SortOrder = 4; } ColumnTypes.Add(Info); } } } ColumnTypes.Sort(); } FString Section = ""; for(FColumnTypeInfoStruct& Type : ColumnTypes) { if (Section != Type.Category) { if (Section != "") { MenuBuilder.EndSection(); } Section = Type.Category; MenuBuilder.BeginSection(FName(Section), FText::FromString(Section)); } MenuBuilder.AddMenuEntry(Type.Type->GetDisplayNameText(), Type.Type->GetToolTipText(), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([this, Type]() { AddColumn(Type.Type); })) ); } return MenuBuilder.MakeWidget(); } struct FResultTypeInfoStruct { bool ObjectOnly = false; bool ClassOnly = false; FString Category; UScriptStruct* Type = nullptr; bool operator < (const FResultTypeInfoStruct& Other) const { if (Category == Other.Category) { return Type->GetDisplayNameText().ToString() < Other.Type->GetDisplayNameText().ToString(); } else { return Category < Other.Category; } } }; void MakeCreateResultMenu(FMenuBuilder& MenuBuilder, EObjectChooserResultType ChooserResultType, FCreateStructDelegate CreateStruct) { static TArray ResultTypes; if (ResultTypes.IsEmpty()) { UScriptStruct* BaseType = FObjectChooserBase::StaticStruct(); for (TObjectIterator StructIt; StructIt; ++StructIt) { if (*StructIt != BaseType && StructIt->IsChildOf(BaseType)) { if (!StructIt->HasMetaData("Hidden")) { FResultTypeInfoStruct Info; Info.Type = *StructIt; Info.Category = StructIt->HasMetaData("Category") ? StructIt->GetMetaData("Category") : "Other"; if (StructIt->HasMetaData("ResultType")) { FString ResultTypeString = StructIt->GetMetaData("ResultType"); Info.ClassOnly = ResultTypeString == "Class"; Info.ObjectOnly = ResultTypeString == "Object"; } ResultTypes.Add(Info); } } } ResultTypes.Sort(); } FString Section = ""; for(FResultTypeInfoStruct& Type : ResultTypes) { if (Section != Type.Category) { if (Section != "") { MenuBuilder.EndSection(); } Section = Type.Category; MenuBuilder.BeginSection(FName(Section), FText::FromString(Section)); } MenuBuilder.AddMenuEntry(Type.Type->GetDisplayNameText(), Type.Type->GetToolTipText(), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([Type, CreateStruct]() { CreateStruct.Execute(Type.Type); }), FCanExecuteAction::CreateLambda([Type, ChooserResultType]() { if (Type.ClassOnly && ChooserResultType == EObjectChooserResultType::ObjectResult) { return false; } if (Type.ObjectOnly && ChooserResultType == EObjectChooserResultType::ClassResult) { return false; } return true; }) ) ); } } TSharedRef FChooserTableEditor::MakeCreateRowMenu() { FMenuBuilder MenuBuilder(true, nullptr); UChooserTable* Chooser = GetChooser(); if (!Chooser->FallbackResult.IsValid()) { if (Chooser->ResultType == EObjectChooserResultType::NoPrimaryResult) { MenuBuilder.AddMenuEntry( LOCTEXT("Add Fallback Output", "Add Fallback Output"), LOCTEXT("Add Fallback Output Tooltip", "Add a Fallback row to the chooser, which will be used in the case where no other rows passed all filter columns"), FSlateIcon(), FUIAction(FExecuteAction::CreateLambda([this]() { UChooserTable* Chooser = GetChooser(); const FScopedTransaction Transaction(LOCTEXT("Add Fallback Row Transaction", "Add Fallback Row")); Chooser->Modify(true); // Just construct a dummy result to make sure all rows always have "valid results" // You can't just leave a null result otherwise rows don't apply their output. Chooser->FallbackResult.InitializeAs(FClassChooser::StaticStruct()); Chooser->FallbackResult.GetMutable().Class = UClass::StaticClass(); UpdateTableRows(); })) ); } else { MenuBuilder.AddSubMenu( LOCTEXT("Add Fallback", "Add Fallback Result"), LOCTEXT("Add Fallback Tooltip", "Add a Fallback row to the chooser, which will be used in the case where no other rows passed all filter columns"), FNewMenuDelegate::CreateLambda([this](FMenuBuilder& MenuBuilder) { UChooserTable* Chooser = GetChooser(); MakeCreateResultMenu(MenuBuilder, Chooser->GetContextOwner()->ResultType, FCreateStructDelegate::CreateLambda([this](UScriptStruct* Type) { UChooserTable* Chooser = GetChooser(); const FScopedTransaction Transaction(LOCTEXT("Add Fallback Row Transaction", "Add Fallback Row")); Chooser->Modify(true); Chooser->FallbackResult.InitializeAs(Type); UpdateTableRows(); })); }) ); } } if (Chooser->ResultType == EObjectChooserResultType::NoPrimaryResult) { MenuBuilder.AddMenuEntry( LOCTEXT("Add Output Row", "Add Output Row"), LOCTEXT("Add Output Row Tooltip", "Add a regular row to the chooser"), FSlateIcon(), FUIAction(FExecuteAction::CreateLambda([this]() { UChooserTable* Chooser = GetChooser(); const FScopedTransaction Transaction(LOCTEXT("Add Row Transaction", "Add Row")); Chooser->Modify(true); // Just construct a dummy result to make sure all rows always have "valid results" // You can't just leave a null result otherwise rows don't apply their output. FInstancedStruct& NewResult = Chooser->ResultsStructs.AddDefaulted_GetRef(); NewResult.InitializeAs(FClassChooser::StaticStruct()); NewResult.GetMutable().Class = UClass::StaticClass(); UpdateTableRows(); })) ); } else { MakeCreateResultMenu(MenuBuilder, Chooser->GetContextOwner()->ResultType, FCreateStructDelegate::CreateLambda([this](UScriptStruct* Type) { UChooserTable* Chooser = GetChooser(); const FScopedTransaction Transaction(LOCTEXT("Add Row Transaction", "Add Row")); Chooser->Modify(true); FInstancedStruct& NewResult = Chooser->ResultsStructs.AddDefaulted_GetRef(); NewResult.InitializeAs(Type); UpdateTableRows(); })); } return MenuBuilder.MakeWidget(); } TSharedRef FChooserTableEditor::SpawnTableTab( const FSpawnTabArgs& Args ) { check( Args.GetTabId() == TableTabId ); UChooserTable* Chooser = GetChooser(); // + button to create new columns CreateColumnComboButton = SNew(SPositiveActionButton) .Text(LOCTEXT("Add Column", "Add Column")) .OnGetMenuContent_Lambda([this]() { return MakeCreateColumnMenu(); }); CreateRowComboButton = SNew(SPositiveActionButton) .Text(LOCTEXT("Add Row", "Add Row")) .OnGetMenuContent(this, &FChooserTableEditor::MakeCreateRowMenu); HeaderRow = SNew(SHeaderRow); UpdateTableRows(); UpdateTableColumns(); TableView = SNew(SListView>) .OnKeyDownHandler_Lambda([this](const FGeometry&, const FKeyEvent& Event) { if (GetToolkitCommands()->ProcessCommandBindings(Event)) { return FReply::Handled(); } return FReply::Unhandled(); }) .ListItemsSource(&TableRows) .OnContextMenuOpening_Raw(this, &FChooserTableEditor::GenerateRowContextMenu) .OnSelectionChanged_Lambda([this](TSharedPtr, ESelectInfo::Type SelectInfo) { // deselect any selected column ClearSelectedColumn(); CurrentSelectionType = ESelectionType::Rows; RefreshRowSelectionDetails(); }) .OnGenerateRow_Raw(this, &FChooserTableEditor::GenerateTableRow) .HeaderRow(HeaderRow); TSharedRef EditChooserTableButton = SNew(SComboButton) .ButtonStyle(FAppStyle::Get(), "GraphBreadcrumbButton"); EditChooserTableButton->SetOnGetMenuContent( FOnGetContent::CreateLambda( [this]() { return MakeChoosersMenu(GetRootChooser()->GetPackage()); }) ); return SNew(SDockTab) .Label( LOCTEXT("ChooserTableTitle", "Chooser Table") ) .TabColorScale( GetTabColorScale() ) .OnCanCloseTab_Lambda([]() { return false; }) [ SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().Padding(3) [ SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth() [ SNew(SButton) .ButtonStyle(FAppStyle::Get(), "GraphBreadcrumbButton") .IsEnabled_Raw(this, &FChooserTableEditor::CanNavigateBack) .OnClicked_Lambda([this]() { NavigateBack(); return FReply::Handled(); }) .Content() [ SNew(SImage) .Image(FAppStyle::Get().GetBrush("Icons.ArrowLeft")) ] ] + SHorizontalBox::Slot().AutoWidth() [ SNew(SButton) .ButtonStyle(FAppStyle::Get(), "GraphBreadcrumbButton") .IsEnabled_Raw(this, &FChooserTableEditor::CanNavigateForward) .OnClicked_Lambda([this]() { NavigateForward(); return FReply::Handled(); }) .Content() [ SNew(SImage) .Image(FAppStyle::Get().GetBrush("Icons.ArrowRight") ) ] ] + SHorizontalBox::Slot().AutoWidth() [ EditChooserTableButton ] + SHorizontalBox::Slot().FillWidth(1) [ BreadcrumbTrail.ToSharedRef() ] ] + SVerticalBox::Slot().FillHeight(1) [ SNew(SScrollBox).Orientation(Orient_Horizontal) + SScrollBox::Slot() [ TableView.ToSharedRef() ] ] ]; } void FChooserTableEditor::UpdateTableRows() { UChooserTable* Chooser = GetChooser(); int32 NewNum = Chooser->ResultsStructs.Num(); Chooser->DisabledRows.SetNum(NewNum); // Sync the TableRows array which drives the ui table to match the number of results. TableRows.SetNum(0, EAllowShrinking::No); for(int i =0; i < NewNum; i++) { TableRows.Add(MakeShared(i)); } // Add one at the end, for the Fallback result if (Chooser->FallbackResult.IsValid()) { TableRows.Add(MakeShared(SChooserTableRow::SpecialIndex_Fallback)); } // Add one at the end, for the "Add Row" control TableRows.Add(MakeShared(SChooserTableRow::SpecialIndex_AddRow)); // Make sure each column has the same number of row datas as there are results for(FInstancedStruct& ColumnData : Chooser->ColumnsStructs) { FChooserColumnBase& Column = ColumnData.GetMutable(); Column.SetNumRows(NewNum); } if (TableView.IsValid()) { TableView->RebuildList(); } } void FChooserTableEditor::OnObjectsReplaced(const TMap& ReplacementMap) { bool bChangedAny = false; UObject* ReplacedObject = ReplacementMap.FindRef(RootChooser); if (ReplacedObject && ReplacedObject != RootChooser) { RootChooser = Cast(ReplacedObject); SetChooserTableToEdit(RootChooser); SelectRootProperties(); } } FString FChooserTableEditor::GetWorldCentricTabPrefix() const { return LOCTEXT("WorldCentricTabPrefix", "Chooser Table Asset ").ToString(); } TSharedRef FChooserTableEditor::CreateEditor( const EToolkitMode::Type Mode, const TSharedPtr< IToolkitHost >& InitToolkitHost, UObject* ObjectToEdit, FGetDetailsViewObjects GetDetailsViewObjects ) { TSharedRef< FChooserTableEditor > NewEditor( new FChooserTableEditor() ); TArray ObjectsToEdit; ObjectsToEdit.Add( ObjectToEdit ); NewEditor->InitEditor( Mode, InitToolkitHost, ObjectsToEdit, GetDetailsViewObjects ); return NewEditor; } TSharedRef FChooserTableEditor::CreateEditor( const EToolkitMode::Type Mode, const TSharedPtr< IToolkitHost >& InitToolkitHost, const TArray& ObjectsToEdit, FGetDetailsViewObjects GetDetailsViewObjects ) { TSharedRef< FChooserTableEditor > NewEditor( new FChooserTableEditor() ); NewEditor->InitEditor( Mode, InitToolkitHost, ObjectsToEdit, GetDetailsViewObjects ); return NewEditor; } void FChooserTableEditor::SelectColumn(UChooserTable* ChooserEditor, int Index) { ClearSelectedRows(); UChooserTable* Chooser = GetChooser(); if (Index < Chooser->ColumnsStructs.Num()) { if (SelectedColumn == nullptr) { SelectedColumn = TStrongObjectPtr(NewObject(GetTransientPackage(), NAME_None, RF_Transactional | RF_Transient)); } SelectedColumn->Chooser = Chooser; SelectedColumn->Column = Index; DetailsView->SetObject(SelectedColumn.Get(), true); CurrentSelectionType = ESelectionType::Column; } else { SelectRootProperties(); } } void FChooserTableEditor::ClearSelectedColumn() { UChooserTable* Chooser = GetChooser(); if (SelectedColumn != nullptr) { SelectedColumn->Column = -1; if (DetailsView->GetSelectedObjects().Contains(SelectedColumn.Get())) { SelectRootProperties(); } } } void FChooserTableEditor::DeleteColumn(int Index) { const FScopedTransaction Transaction(LOCTEXT("Delete Column Transaction", "Delete Column")); ClearSelectedColumn(); SelectRootProperties(); UChooserTable* Chooser = GetChooser(); if (Index < Chooser->ColumnsStructs.Num()) { Chooser->Modify(true); Chooser->ColumnsStructs.RemoveAt(Index); UpdateTableColumns(); } } int32 FChooserTableEditor::DeleteSelectedRows(int32 RowIndexToRemember) { const FScopedTransaction Transaction(LOCTEXT("Delete Row Transaction", "Delete Row")); return DeleteSelectedRowsInternal(RowIndexToRemember); } int32 FChooserTableEditor::DeleteSelectedRowsInternal(int32 RowIndexToRemember) { UChooserTable* Chooser = GetChooser(); Chooser->Modify(true); // delete selected rows. TArray RowsToDelete; for(auto& SelectedRow:SelectedRows) { if (SelectedRow->Row == ColumnWidget_SpecialIndex_Fallback) { Chooser->FallbackResult.Reset(); } else { RowsToDelete.Add(SelectedRow->Row); } } SelectedRows.SetNum(0); SelectRootProperties(); // sort indices in reverse RowsToDelete.Sort([](int32 A, int32 B){ return A>B; }); for(int32 RowIndex : RowsToDelete) { if (RowIndex <= RowIndexToRemember) { RowIndexToRemember--; } Chooser->ResultsStructs.RemoveAt(RowIndex); Chooser->DisabledRows.RemoveAt(RowIndex); } for(FInstancedStruct& ColumnData : Chooser->ColumnsStructs) { FChooserColumnBase& Column = ColumnData.GetMutable(); Column.DeleteRows(RowsToDelete); } UpdateTableRows(); return RowIndexToRemember; } void FChooserTableEditor::MoveRows(int TargetIndex) { const FScopedTransaction Transaction(LOCTEXT("Move Row(s)", "Move Row(s)")); UChooserTable* RowCopy = CopySelectionInternal(); TargetIndex = DeleteSelectedRowsInternal(TargetIndex); PasteInternal(RowCopy, TargetIndex); } void FChooserTableEditor::AutoPopulateColumn(FChooserColumnBase& Column) { if (UChooserTable* Chooser = GetChooser()) { const int RowCount = Chooser->ResultsStructs.Num(); if (Column.AutoPopulates()) { for (int i = 0; i < RowCount; ++i) { UObject* ReferencedObject = Chooser->ResultsStructs[i].IsValid() ? Chooser->ResultsStructs[i].Get().GetReferencedObject() : nullptr; Column.AutoPopulate(i, ReferencedObject); } UObject* ReferencedObject = Chooser->FallbackResult.IsValid() ? Chooser->FallbackResult.Get().GetReferencedObject() : nullptr; Column.AutoPopulate(ColumnWidget_SpecialIndex_Fallback, ReferencedObject); } } } void FChooserTableEditor::AutoPopulateRow(int Index) { if (UChooserTable* Chooser = GetChooser()) { if (Chooser->ResultsStructs.IsValidIndex(Index) && Chooser->ResultsStructs[Index].IsValid()) { if (UObject* ReferencedObject = Chooser->ResultsStructs[Index].Get().GetReferencedObject()) { for (FInstancedStruct& ColumnData : Chooser->ColumnsStructs) { FChooserColumnBase& Column = ColumnData.GetMutable(); Column.AutoPopulate(Index, ReferencedObject); } } } else if (Index == ColumnWidget_SpecialIndex_Fallback && Chooser->FallbackResult.IsValid()) { if (UObject* ReferencedObject = Chooser->FallbackResult.Get().GetReferencedObject()) { for (FInstancedStruct& ColumnData : Chooser->ColumnsStructs) { FChooserColumnBase& Column = ColumnData.GetMutable(); Column.AutoPopulate(Index, ReferencedObject); } } } } } bool FChooserTableEditor::CanAutoPopulateSelection() { if (UChooserTable* Chooser = GetChooser()) { if (CurrentSelectionType == ESelectionType::Column && SelectedColumn) { // when a column is selected, return true if that column supports auto populate if (Chooser->ColumnsStructs.IsValidIndex(SelectedColumn->Column)) { return Chooser->ColumnsStructs[SelectedColumn->Column].Get().AutoPopulates(); } } else { if (SelectedRows.IsEmpty()) { return false; } // when rows are selected, return true if any column supports auto populate for (FInstancedStruct& ColumnData : Chooser->ColumnsStructs) { const FChooserColumnBase& Column = ColumnData.Get(); if (Column.AutoPopulates()) { return true; } } } } return false; } void FChooserTableEditor::AutoPopulateSelection() { if (UChooserTable* Chooser = GetChooser()) { const FScopedTransaction Transaction(LOCTEXT("Auto Populate Selection", "Auto Populate Selection")); Chooser->Modify(); if (HasColumnSelected()) { if (Chooser->ColumnsStructs.IsValidIndex(SelectedColumn->Column)) { AutoPopulateColumn(Chooser->ColumnsStructs[SelectedColumn->Column].GetMutable()); } } else if (HasRowsSelected()) { for(UChooserRowDetails* RowDetails : SelectedRows) { AutoPopulateRow(RowDetails->Row); } } } } void FChooserTableEditor::AutoPopulateAll() { if (UChooserTable* Chooser = GetChooser()) { const FScopedTransaction Transaction(LOCTEXT("Auto Populate Chooser", "Auto Populate All")); Chooser->Modify(); for (FInstancedStruct& ColumnData : Chooser->ColumnsStructs) { FChooserColumnBase& Column = ColumnData.GetMutable(); AutoPopulateColumn(Column); } } } bool FChooserTableEditor::HasSelection() const { return HasRowsSelected() || HasColumnSelected(); } bool FChooserTableEditor::HasRowsSelected() const { return CurrentSelectionType == ESelectionType::Rows && !SelectedRows.IsEmpty(); } bool FChooserTableEditor::HasColumnSelected() const { const UChooserTable* Chooser = GetChooser(); return CurrentSelectionType == ESelectionType::Column && SelectedColumn && Chooser->ColumnsStructs.IsValidIndex(SelectedColumn->Column); } bool FChooserTableEditor::IsSelectionDisabled() const { if (const UChooserTable* Chooser = GetChooser()) { if (HasColumnSelected()) { if ( Chooser->ColumnsStructs.IsValidIndex(SelectedColumn->Column)) { const FChooserColumnBase& Column = Chooser->ColumnsStructs[SelectedColumn->Column].Get(); return Column.bDisabled; } } else if (HasRowsSelected()) { bool bSomethingEnabled = false; for(auto& Row : SelectedRows) { if (!Chooser->IsRowDisabled(Row->Row)) { bSomethingEnabled = true; break; } } return !bSomethingEnabled; } } return false; } void FChooserTableEditor::ToggleDisableSelection() { bool bDisabled = IsSelectionDisabled(); if (UChooserTable* Chooser = GetChooser()) { if (HasColumnSelected()) { if (Chooser->ColumnsStructs.IsValidIndex(SelectedColumn->Column)) { FChooserColumnBase& Column = Chooser->ColumnsStructs[SelectedColumn->Column].GetMutable(); Column.bDisabled = !Column.bDisabled; } } else if (HasRowsSelected()) { for (auto& Row : SelectedRows) { if (Chooser->DisabledRows.IsValidIndex(Row->Row)) { Chooser->DisabledRows[Row->Row] = !bDisabled; } } RefreshRowSelectionDetails(); } } } void FChooserTableEditor::DeleteSelection() { if (HasColumnSelected()) { DeleteColumn(SelectedColumn->Column); } else if (HasRowsSelected()) { DeleteSelectedRows(); } } void FChooserTableEditor::DuplicateSelection() { if (HasRowsSelected()) { const FScopedTransaction Transaction(LOCTEXT("Duplicate Row(s)", "Duplicate Row(s)")); UChooserTable* RowCopy = CopySelectionInternal(); int MaxSelectedRow = -1; for(UChooserRowDetails* SelectedRow : SelectedRows) { MaxSelectedRow = FMath::Max(SelectedRow->Row, MaxSelectedRow); } PasteInternal(RowCopy, MaxSelectedRow + 1); } else if(HasColumnSelected()) { const FScopedTransaction Transaction(LOCTEXT("Duplicate Column", "Duplicate Column")); UChooserTable* Chooser = GetChooser(); Chooser->Modify(); FInstancedStruct Column = Chooser->ColumnsStructs[SelectedColumn->Column]; Chooser->ColumnsStructs.Insert(Column,SelectedColumn->Column); RefreshAll(); } } bool FChooserTableEditor::HasFallbackSelected() { for(UChooserRowDetails* SelectedRow : SelectedRows) { if (SelectedRow->Row == ColumnWidget_SpecialIndex_Fallback) { return true; } } return false; } bool FChooserTableEditor::CanMoveRowsUp() { if (HasRowsSelected()) { UChooserTable* Chooser = GetChooser(); int MinSelectedRow = Chooser->ResultsStructs.Num(); for(UChooserRowDetails* SelectedRow : SelectedRows) { if (SelectedRow->Row != ColumnWidget_SpecialIndex_Fallback) { MinSelectedRow = FMath::Min(SelectedRow->Row, MinSelectedRow); } } return MinSelectedRow > 0; } return false; } void FChooserTableEditor::MoveRowsUp() { if (HasRowsSelected()) { UChooserTable* Chooser = GetChooser(); int MinSelectedRow = Chooser->ResultsStructs.Num(); for(UChooserRowDetails* SelectedRow : SelectedRows) { if (SelectedRow->Row != ColumnWidget_SpecialIndex_Fallback) { MinSelectedRow = FMath::Min(SelectedRow->Row, MinSelectedRow); } } MoveRows(MinSelectedRow - 1); } } bool FChooserTableEditor::CanMoveRowsDown() { if (HasRowsSelected()) { UChooserTable* Chooser = GetChooser(); int MaxSelectedRow = -1; for(UChooserRowDetails* SelectedRow : SelectedRows) { MaxSelectedRow = FMath::Max(SelectedRow->Row, MaxSelectedRow); } return MaxSelectedRow < Chooser->ResultsStructs.Num() - 1; } return false; } void FChooserTableEditor::MoveRowsDown() { if (HasRowsSelected()) { UChooserTable* Chooser = GetChooser(); int MaxSelectedRow = -1; for(UChooserRowDetails* SelectedRow : SelectedRows) { MaxSelectedRow = FMath::Max(SelectedRow->Row, MaxSelectedRow); } MoveRows(MaxSelectedRow + 2); } } bool FChooserTableEditor::CanMoveColumnLeft() { if (HasColumnSelected()) { UChooserTable* Chooser = GetChooser(); if (Chooser->ColumnsStructs[SelectedColumn->Column].GetPtr()) { return false; } return SelectedColumn->Column > 0; } return false; } void FChooserTableEditor::MoveColumnLeft() { if (CanMoveColumnLeft()) { SelectColumn(GetChooser(), MoveColumn(SelectedColumn->Column, SelectedColumn->Column - 1)); } } bool FChooserTableEditor::CanMoveColumnRight() { if (HasColumnSelected()) { UChooserTable* Chooser = GetChooser(); if (Chooser->ColumnsStructs[SelectedColumn->Column].GetPtr()) { return false; } int NumColumns = Chooser->ColumnsStructs.Num(); if (NumColumns > 0 && Chooser->ColumnsStructs.Last().GetPtr()) { NumColumns--; } return (SelectedColumn->Column < NumColumns-1); } return false; } void FChooserTableEditor::MoveColumnRight() { if (CanMoveColumnRight()) { SelectColumn(GetChooser(), MoveColumn(SelectedColumn->Column, SelectedColumn->Column + 2)); } } UChooserTable* DuplicateNestedChooser(UChooserTable* Chooser, UChooserTable* NewOuter) { UChooserTable* RootTable = NewOuter->GetRootChooser(); if (TObjectPtr* FoundTable = RootTable->NestedObjects.FindByPredicate([Chooser](UObject* Object) { if (Cast(Object)) { return Object->GetName() == Chooser->GetName(); } return false; })) { // we already duplicated this table return Cast(*FoundTable); } UChooserTable* NewTable = NewObject(NewOuter, Chooser->GetFName(), RF_Transactional); NewTable->ColumnsStructs = Chooser->ColumnsStructs; NewTable->ResultsStructs = Chooser->ResultsStructs; NewTable->RootChooser = RootTable; RootTable->AddNestedObject(NewTable); for (FInstancedStruct& ResultData : NewTable->ResultsStructs) { if (FNestedChooser* NestedChooser = ResultData.GetMutablePtr()) { NestedChooser->Chooser = DuplicateNestedChooser(NestedChooser->Chooser, NewOuter); } } return NewTable; } UChooserTable* FChooserTableEditor::CopySelectionInternal() { UChooserTable* CopyData = NewObject(GetTransientPackage()); UChooserTable* Chooser = GetChooser(); // copy context data from root table CopyData->OutputObjectType = RootChooser->OutputObjectType; CopyData->ResultType = RootChooser->ResultType; CopyData->ContextData = RootChooser->ContextData; if (CurrentSelectionType == ESelectionType::Column) { // add selected column including all the cell data CopyData->ColumnsStructs.Add(Chooser->ColumnsStructs[SelectedColumn->Column]); } else if (CurrentSelectionType == ESelectionType::Rows) { TArray SelectedRowsCopy = SelectedRows; Algo::Sort(SelectedRowsCopy, [](UChooserRowDetails *A, UChooserRowDetails *B) { return A->RowRow;}); if (SelectedRowsCopy.Num() > 0 && SelectedRowsCopy[0]->Row == ColumnWidget_SpecialIndex_Fallback) { SelectedRowsCopy.RemoveAt(0); CopyData->FallbackResult = Chooser->FallbackResult; if (FNestedChooser* CopiedNestedChooser = CopyData->FallbackResult.GetMutablePtr()) { // if the fallback result was a nested chooser, duplicate it CopiedNestedChooser->Chooser = DuplicateNestedChooser(CopiedNestedChooser->Chooser, CopyData); } } CopyData->ResultsStructs.SetNum(SelectedRowsCopy.Num()); CopyData->DisabledRows.SetNum(SelectedRowsCopy.Num()); // add the selected results and column data for (int RowIndex = 0; RowIndex < SelectedRowsCopy.Num(); RowIndex++) { CopyData->ResultsStructs[RowIndex] = Chooser->ResultsStructs[SelectedRowsCopy[RowIndex]->Row]; if (FNestedChooser* CopiedNestedChooser = CopyData->ResultsStructs[RowIndex].GetMutablePtr()) { if (CopiedNestedChooser->Chooser) { // if the result for this row was a nested chooser (with a valid chooser assigned), duplicate it CopiedNestedChooser->Chooser = DuplicateNestedChooser(CopiedNestedChooser->Chooser, CopyData); } } CopyData->DisabledRows[RowIndex] = Chooser->DisabledRows[SelectedRowsCopy[RowIndex]->Row]; } // copy all columns CopyData->ColumnsStructs = Chooser->ColumnsStructs; for (FInstancedStruct& ColumnData : CopyData->ColumnsStructs) { FChooserColumnBase& Column = ColumnData.GetMutable(); Column.Initialize(CopyData); } // clear all column's cell data for (FInstancedStruct& ColumnData : CopyData->ColumnsStructs) { FChooserColumnBase& Column = ColumnData.GetMutable(); Column.SetNumRows(0); Column.SetNumRows(SelectedRowsCopy.Num()); } for (int RowIndex = 0; RowIndex < SelectedRowsCopy.Num(); RowIndex++) { for (int ColumnIndex = 0; ColumnIndex < CopyData->ColumnsStructs.Num(); ColumnIndex++) { FChooserColumnBase& SourceColumn = Chooser->ColumnsStructs[ColumnIndex].GetMutable(); FChooserColumnBase& TargetColumn = CopyData->ColumnsStructs[ColumnIndex].GetMutable(); TargetColumn.CopyRow(SourceColumn, SelectedRowsCopy[RowIndex]->Row, RowIndex); } } } return CopyData; } void FChooserTableEditor::CopySelection() { UChooserTable* CopyData = CopySelectionInternal(); // Clear the mark state for saving. UnMarkAllObjects(EObjectMark(OBJECTMARK_TagExp | OBJECTMARK_TagImp)); // Export the clipboard to text. FStringOutputDevice Archive; const FExportObjectInnerContext Context; UExporter::ExportToOutputDevice(&Context,CopyData, nullptr, Archive, TEXT("copy"), 0, PPF_ExportsNotFullyQualified | PPF_Copy | PPF_Delimited, false, CopyData->GetOuter()); FPlatformApplicationMisc::ClipboardCopy(*Archive); } void FChooserTableEditor::CutSelection() { CopySelection(); DeleteSelection(); } class FChooserClipboardFactory : public FCustomizableTextObjectFactory { public: FChooserClipboardFactory() : FCustomizableTextObjectFactory(GWarn) , ClipboardContent(nullptr) { } UChooserTable* ClipboardContent; protected: virtual bool CanCreateClass(UClass* InObjectClass, bool& bOmitSubObjs) const override { if (InObjectClass->IsChildOf(UChooserTable::StaticClass())) { return true; } return false; } virtual void ProcessConstructedObject(UObject* CreatedObject) override { if (CreatedObject->IsA()) { ClipboardContent = CastChecked(CreatedObject); } } }; static FString GetColumnName(FChooserColumnBase& Column) { if (FChooserParameterBase* InputValue = Column.GetInputValue()) { return InputValue->GetDebugName(); } return FString(); } void FChooserTableEditor::PasteInternal(UChooserTable* PastedContent, int PasteRowIndex) { UChooserTable* Chooser = GetChooser(); Chooser->Modify(); if (PastedContent->ResultsStructs.IsEmpty() && !PastedContent->FallbackResult.IsValid()) { // pasting a column int InsertColumnIndex = Chooser->ColumnsStructs.Num(); if (CurrentSelectionType == ESelectionType::Column && SelectedColumn) { InsertColumnIndex = FMath::Min(InsertColumnIndex, SelectedColumn->Column + 1); } if (Chooser->ColumnsStructs.Num() > 0 && Chooser->ColumnsStructs.Num() == InsertColumnIndex) { // if were inserting at the end, there is a randomize column, insert new columns before it if (Chooser->ColumnsStructs.Last().GetPtr()) { InsertColumnIndex--; } } Chooser->ColumnsStructs.Insert(PastedContent->ColumnsStructs, InsertColumnIndex); SelectColumn(Chooser, InsertColumnIndex); } else { // pasting rows int RowsToPaste = PastedContent->ResultsStructs.Num(); // figure out where to start inserting int InsertIndex = Chooser->ResultsStructs.Num(); if (PasteRowIndex >=0) { InsertIndex = PasteRowIndex; } else { if (SelectedRows.Num() > 0) { InsertIndex = SelectedRows[0]->Row; for(int SelectedRowIndex = 1; SelectedRowIndex < SelectedRows.Num(); SelectedRowIndex ++) { InsertIndex = FMath::Max(InsertIndex, SelectedRows[SelectedRowIndex]->Row); } if (InsertIndex == ColumnWidget_SpecialIndex_Fallback) { // if the only row selected was the fallback, reset insert index to the last row InsertIndex = Chooser->ResultsStructs.Num(); } else { InsertIndex++; } } } if (PastedContent->ResultsStructs.Num() > 0) { Chooser->ResultsStructs.Insert(PastedContent->ResultsStructs, InsertIndex); Chooser->DisabledRows.Insert(PastedContent->DisabledRows, InsertIndex); // Make sure each column has the same number of row datas as there are results for(FInstancedStruct& ColumnData : Chooser->ColumnsStructs) { FChooserColumnBase& Column = ColumnData.GetMutable(); Column.InsertRows(InsertIndex, RowsToPaste); } } if (PastedContent->FallbackResult.IsValid()) { // paste fallback result if copy data has one Chooser->FallbackResult = PastedContent->FallbackResult; if (FNestedChooser* NestedChooser = Chooser->FallbackResult.GetMutablePtr()) { // duplicate the nested chooser if the fallback result refers to a nested chooser NestedChooser->Chooser = DuplicateNestedChooser(NestedChooser->Chooser, Chooser); } } if (!PastedContent->NestedObjects.IsEmpty()) { // if there were nested choosers in the copy buffer we have to remap or paste them here for (int ResultIndex = InsertIndex; ResultIndex < PastedContent->ResultsStructs.Num() + InsertIndex; ResultIndex++) { if (FNestedChooser* NestedChooser = Chooser->ResultsStructs[ResultIndex].GetMutablePtr()) { NestedChooser->Chooser = DuplicateNestedChooser(NestedChooser->Chooser, Chooser); } } } // try to also paste column data from columns in the paste buffer which match the columns in the current chooser // -- matching by column type and input value name // keep track of target columns that have already been matched, to avoid matching multiple source columns with the same target column TArray MatchedTargetColumns; MatchedTargetColumns.SetNum(Chooser->ColumnsStructs.Num()); // keep track of which source columns were matched, so we can add new columns for the unmatched ones after TArray MatchedSourceColumns; MatchedSourceColumns.SetNum(PastedContent->ColumnsStructs.Num()); for(int SourceColumnIndex = 0; SourceColumnIndex < PastedContent->ColumnsStructs.Num(); SourceColumnIndex++) { FInstancedStruct& PastedColumnData = PastedContent->ColumnsStructs[SourceColumnIndex]; FChooserColumnBase& PastedColumn = PastedColumnData.GetMutable(); FString PastedColumnName = GetColumnName(PastedColumn); for(int TargetColumnIndex = 0; TargetColumnIndex < Chooser->ColumnsStructs.Num(); TargetColumnIndex++) { if (!MatchedTargetColumns[TargetColumnIndex]) { FInstancedStruct& ColumnData = Chooser->ColumnsStructs[TargetColumnIndex]; if (ColumnData.GetScriptStruct() == PastedColumnData.GetScriptStruct()) { FChooserColumnBase& Column = ColumnData.GetMutable(); FString ColumnName = GetColumnName(Column); if (ColumnName == PastedColumnName) { MatchedTargetColumns[TargetColumnIndex] = true; MatchedSourceColumns[SourceColumnIndex] = true; // found a match, copy the data over for (int i = 0; i < RowsToPaste; i++) { Column.CopyRow(PastedColumn, i, InsertIndex + i); } if (PastedContent->FallbackResult.IsValid()) { // if the fallback row was copied, paste the fallback data for columns Column.CopyFallback(PastedColumn); } break; } } } } } // add new columns for any source columns that were unmatched int InsertColumnIndex = Chooser->ColumnsStructs.Num(); if (Chooser->ColumnsStructs.Num() > 0) { // if there is a randomize column, insert new columns before it if (Chooser->ColumnsStructs.Last().GetPtr()) { InsertColumnIndex--; } } for(int SourceColumnIndex = 0; SourceColumnIndex < PastedContent->ColumnsStructs.Num(); SourceColumnIndex++) { if (!MatchedSourceColumns[SourceColumnIndex]) { FInstancedStruct& PastedColumnData = PastedContent->ColumnsStructs[SourceColumnIndex]; FChooserColumnBase& PastedColumn = PastedColumnData.GetMutable(); // if we couldn't find a match, paste a new column Chooser->ColumnsStructs.Insert(PastedColumnData, InsertColumnIndex); FChooserColumnBase& Column = Chooser->ColumnsStructs[InsertColumnIndex].GetMutable(); InsertColumnIndex++; Column.SetNumRows(0); Column.SetNumRows(Chooser->ResultsStructs.Num()); for (int i = 0; i < RowsToPaste; i++) { Column.CopyRow(PastedColumn, i, InsertIndex + i); } } } ClearSelectedRows(); RefreshAll(); TArray> SelectedIndices; SelectedIndices.Reserve(RowsToPaste+1); // select the inserted rows for (int i = 0; i < RowsToPaste; i++) { SelectedIndices.Add(InsertIndex + i); } if(PastedContent->FallbackResult.IsValid()) { SelectedIndices.Add(ColumnWidget_SpecialIndex_Fallback); } SelectRows(SelectedIndices); } } void FChooserTableEditor::Paste() { // Get the text from the clipboard. FString ClipboardText; FPlatformApplicationMisc::ClipboardPaste(ClipboardText); FChooserClipboardFactory Factory; UChooserTable* Chooser = GetChooser(); if (Factory.CanCreateObjectsFromText(ClipboardText)) { Factory.ProcessBuffer((UObject*)GetTransientPackage(), RF_Transactional, ClipboardText); if (UChooserTable* PastedContent = Factory.ClipboardContent) { FScopedTransaction Transaction(LOCTEXT("Paste Chooser Data", "Paste Chooser Data")); PasteInternal(PastedContent); } } } bool FChooserTableEditor::CanPaste() const { // Get the text from the clipboard. FString ClipboardText; FPlatformApplicationMisc::ClipboardPaste(ClipboardText); FChooserClipboardFactory Factory; return Factory.CanCreateObjectsFromText(ClipboardText); } void FChooserTableEditor::RegisterWidgets() { FObjectChooserWidgetFactories::RegisterWidgetCreator(FAssetChooser::StaticStruct(), CreateAssetWidget); FObjectChooserWidgetFactories::RegisterWidgetCreator(FSoftAssetChooser::StaticStruct(), CreateSoftAssetWidget); FObjectChooserWidgetFactories::RegisterWidgetCreator(FClassChooser::StaticStruct(), CreateClassWidget); FObjectChooserWidgetFactories::RegisterWidgetCreator(FEvaluateChooser::StaticStruct(), CreateEvaluateChooserWidget); FObjectChooserWidgetFactories::RegisterWidgetCreator(FNestedChooser::StaticStruct(), CreateNestedChooserWidget); FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); PropertyModule.RegisterCustomClassLayout("ChooserTable", FOnGetDetailCustomizationInstance::CreateStatic(&FChooserDetails::MakeInstance)); PropertyModule.RegisterCustomClassLayout("ChooserRowDetails", FOnGetDetailCustomizationInstance::CreateStatic(&FChooserRowDetails::MakeInstance)); PropertyModule.RegisterCustomClassLayout("ChooserColumnDetails", FOnGetDetailCustomizationInstance::CreateStatic(&FChooserColumnDetails::MakeInstance)); } } #undef LOCTEXT_NAMESPACE