// Copyright Epic Games, Inc. All Rights Reserved. #include "CurveTableEditor.h" #include "Containers/ArrayView.h" #include "CurveEditor.h" #include "CurveModel.h" #include "CurveTableEditorCommands.h" #include "CurveTableEditorHandle.h" #include "CurveTableEditorModule.h" #include "Curves/KeyHandle.h" #include "Curves/SimpleCurve.h" #include "Editor.h" #include "Editor/EditorEngine.h" #include "EditorReimportHandler.h" #include "Engine/CompositeCurveTable.h" #include "Engine/CurveTable.h" #include "Fonts/FontMeasure.h" #include "Framework/Application/SlateApplication.h" #include "Framework/Commands/GenericCommands.h" #include "Framework/Commands/UIAction.h" #include "Framework/Commands/UICommandList.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Framework/MultiBox/MultiBoxDefs.h" #include "Framework/MultiBox/MultiBoxExtender.h" #include "Framework/Text/TextLayout.h" #include "Framework/Views/ITypedTableView.h" #include "ICurveEditorModule.h" #include "Internationalization/Internationalization.h" #include "Layout/BasicLayoutWidgetSlot.h" #include "Layout/Margin.h" #include "Math/UnrealMathSSE.h" #include "Math/Vector2D.h" #include "Misc/AssertionMacros.h" #include "Misc/Attribute.h" #include "Modules/ModuleManager.h" #include "RealCurveModel.h" #include "Rendering/SlateRenderer.h" #include "RichCurveEditorModel.h" #include "SCurveEditorPanel.h" #include "SPositiveActionButton.h" #include "ScopedTransaction.h" #include "SlotBase.h" #include "Styling/AppStyle.h" #include "Styling/ISlateStyle.h" #include "Styling/SlateColor.h" #include "Styling/SlateTypes.h" #include "Styling/StyleColors.h" #include "Templates/Casts.h" #include "Templates/Tuple.h" #include "Templates/UniquePtr.h" #include "Textures/SlateIcon.h" #include "Toolkits/AssetEditorToolkit.h" #include "Tree/CurveEditorTree.h" #include "Tree/CurveEditorTreeFilter.h" #include "Tree/CurveEditorTreeTraits.h" #include "Tree/ICurveEditorTreeItem.h" #include "Tree/SCurveEditorTree.h" #include "Tree/SCurveEditorTreePin.h" #include "Tree/SCurveEditorTreeSelect.h" #include "Tree/SCurveEditorTreeTextFilter.h" #include "Types/SlateStructs.h" #include "UObject/ObjectMacros.h" #include "UObject/UnrealNames.h" #include "UObject/UnrealType.h" #include "UObject/WeakObjectPtrTemplates.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/Docking/SDockTab.h" #include "Widgets/Input/SNumericEntryBox.h" #include "Widgets/Input/SSegmentedControl.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SScrollBar.h" #include "Widgets/Layout/SScrollBox.h" #include "Widgets/Layout/SSplitter.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SNullWidget.h" #include "Widgets/Text/SInlineEditableTextBlock.h" #include "Widgets/Views/SListView.h" class ITableRow; class SWidget; class UObject; struct FRichCurve; #define LOCTEXT_NAMESPACE "CurveTableEditor" const FName FCurveTableEditor::CurveTableTabId("CurveTableEditor_CurveTable"); struct FCurveTableEditorColumnHeaderData { /** Unique ID used to identify this column */ FName ColumnId; /** Display name of this column */ FText DisplayName; /** The calculated width of this column taking into account the cell data for each row */ float DesiredColumnWidth; /** The evaluated key time **/ float KeyTime; }; namespace { FName MakeUniqueCurveName( UCurveTable* Table ) { check(Table != nullptr); int incr = 0; FName TestName = FName("Curve", incr); const TMap& RowMap = Table->GetRowMap(); while (RowMap.Contains(TestName)) { TestName = FName("Curve", ++incr); } return TestName; } } /* * FCurveTableEditorItem * * FCurveTableEditorItem uses and extends the CurveEditorTreeItem to be used in both our TableView and the CurveEditorTree. * The added GenerateTableViewCell handles the table columns unknown to the standard CurveEditorTree. * */ class FCurveTableEditorItem : public ICurveEditorTreeItem, public TSharedFromThis { struct CachedKeyInfo { CachedKeyInfo(FKeyHandle& InKeyHandle, FText InDisplayValue) : KeyHandle(InKeyHandle) , DisplayValue(InDisplayValue) {} FKeyHandle KeyHandle; FText DisplayValue; }; public: FCurveTableEditorItem (TWeakPtr InCurveTableEditor, const FCurveEditorTreeItemID& InTreeID, const FName& InRowId, FCurveTableEditorHandle InRowHandle, const TArray& InColumns) : CurveTableEditor(InCurveTableEditor) , TreeID(InTreeID) , RowId(InRowId) , RowHandle(InRowHandle) , Columns(InColumns) { DisplayName = FText::FromName(InRowId); CacheKeys(); } TSharedPtr GenerateCurveEditorTreeWidget(const FName& InColumnName, TWeakPtr InCurveEditor, FCurveEditorTreeItemID InTreeItemID, const TSharedRef& InTableRow) override { if (InColumnName == ColumnNames.Label) { return SNew(SHorizontalBox) +SHorizontalBox::Slot() .Padding(FMargin(4.f)) .VAlign(VAlign_Center) .HAlign(HAlign_Right) .AutoWidth() [ SAssignNew(InlineRenameWidget, SInlineEditableTextBlock) .Text(DisplayName) .ColorAndOpacity(FSlateColor::UseForeground()) .MaximumLength(NAME_SIZE-1) .OnTextCommitted(this, &FCurveTableEditorItem::HandleNameCommitted) .OnVerifyTextChanged(this, &FCurveTableEditorItem::VerifyNameChanged) ]; } else if (InColumnName == ColumnNames.SelectHeader) { return SNew(SCurveEditorTreeSelect, InCurveEditor, InTreeItemID, InTableRow); } else if (InColumnName == ColumnNames.PinHeader) { return SNew(SCurveEditorTreePin, InCurveEditor, InTreeItemID, InTableRow); } return GenerateTableViewCell(InColumnName, InCurveEditor, InTreeItemID, InTableRow); } TSharedPtr GenerateTableViewCell(const FName& InColumnId, TWeakPtr InCurveEditor, FCurveEditorTreeItemID InTreeItemID, const TSharedRef& InTableRow) { if (!RowHandle.HasRichCurves()) { FRealCurve* Curve = RowHandle.GetCurve(); FKeyHandle& KeyHandle = CellDataMap[InColumnId].KeyHandle; return SNew(SNumericEntryBox) .EditableTextBoxStyle( &FAppStyle::Get().GetWidgetStyle("CurveTableEditor.Cell.Text") ) .Value_Lambda([this, KeyHandle] () { if (FRealCurve* Curve = RowHandle.GetCurve()) { return Curve->GetKeyValue(KeyHandle); } return 0.0f; }) .OnValueChanged_Lambda([this, KeyHandle] (float NewValue) { if (FRealCurve* Curve = RowHandle.GetCurve()) { FScopedTransaction Transaction(LOCTEXT("SetKeyValues", "Set Key Values")); RowHandle.ModifyOwner(); Curve->SetKeyValue(KeyHandle, NewValue); } }) .Justification(ETextJustify::Right) ; } return SNullWidget::NullWidget; } void CreateCurveModels(TArray>& OutCurveModels) override { if (RowHandle.HasRichCurves()) { if (FRichCurve* RichCurve = RowHandle.GetRichCurve()) { const UCurveTable* Table = RowHandle.CurveTable.Get(); UCurveTable* RawTable = const_cast(Table); TUniquePtr NewCurve = MakeUnique(RichCurve, RawTable); NewCurve->SetShortDisplayName(DisplayName); NewCurve->SetColor(FStyleColors::AccentOrange.GetSpecifiedColor()); OutCurveModels.Add(MoveTemp(NewCurve)); } } else { const UCurveTable* Table = RowHandle.CurveTable.Get(); UCurveTable* RawTable = const_cast(Table); TUniquePtr NewCurveModel = MakeUnique(RowHandle.GetCurve(), RawTable); NewCurveModel->SetShortDisplayName(DisplayName); OutCurveModels.Add(MoveTemp(NewCurveModel)); } } bool PassesFilter(const FCurveEditorTreeFilter* InFilter) const override { if (InFilter->GetType() == ECurveEditorTreeFilterType::Text) { FString DisplayNameAsString = DisplayName.ToString(); const FCurveEditorTreeTextFilter* Filter = static_cast(InFilter); for (const FCurveEditorTreeTextFilterTerm& Term : Filter->GetTerms()) { if (!Term.Match(DisplayNameAsString).IsTotalMatch()) { return false; } } return true; } return false; } void CacheKeys() { if (!RowHandle.HasRichCurves()) { if (FRealCurve* Curve = RowHandle.GetCurve()) { for (auto Col : Columns) { FKeyHandle KeyHandle = Curve->FindKey(Col->KeyTime); float KeyValue = Curve->GetKeyValue(KeyHandle); CellDataMap.Add(Col->ColumnId, CachedKeyInfo(KeyHandle, FText::AsNumber(KeyValue))); } } } } void EnterRenameMode() { InlineRenameWidget->EnterEditingMode(); } bool VerifyNameChanged(const FText& InText, FText& OutErrorMessage) { FName CheckName = FName(*InText.ToString()); if (CheckName == RowId) { return true; } if (RowHandle.CurveTable.IsValid()) { UCurveTable* Table = RowHandle.CurveTable.Get(); const TMap& RowMap = Table->GetRowMap(); if (RowMap.Contains(CheckName)) { OutErrorMessage = LOCTEXT("NameAlreadyUsed", "Row Names Must Be Unique"); return false; } return true; } return false; } void HandleNameCommitted(const FText& CommittedText, ETextCommit::Type CommitInfo) { if (CommitInfo == ETextCommit::OnEnter) { TSharedPtr TableEditorPtr = CurveTableEditor.Pin(); if (TableEditorPtr != nullptr) { FName OldName = RowId; FName NewName = *CommittedText.ToString(); DisplayName = CommittedText; InlineRenameWidget->SetText(DisplayName); RowHandle.RowName = NewName; RowId = NewName; TableEditorPtr->HandleCurveRename(TreeID, OldName, NewName); TSharedPtr CurveEditor = TableEditorPtr->GetCurveEditor(); FCurveEditorTreeItem& TreeItem = CurveEditor->GetTreeItem(TreeID); for (FCurveModelID ModelID : TreeItem.GetCurves()) { if (FCurveModel* CurveModel = CurveEditor->FindCurve(ModelID)) { CurveModel->SetShortDisplayName(DisplayName); } } } } } /** Hold onto a weak ptr to the CurveTableEditor specifically for deleting and renaming */ TWeakPtr CurveTableEditor; /** The CurveEditor's Unique ID for the TreeItem this item is attached to (SetStrongItem) */ FCurveEditorTreeItemID TreeID; /** Unique ID used to identify this row */ FName RowId; /** Display name of this row */ FText DisplayName; /** Array corresponding to each cell in this row */ TMap CellDataMap; /** Handle to the row */ FCurveTableEditorHandle RowHandle; /** A Reference to the available columns in the TableView */ const TArray& Columns; /** Inline editable text box for renaming */ TSharedPtr InlineRenameWidget; }; void FCurveTableEditor::RegisterTabSpawners(const TSharedRef& InTabManager) { WorkspaceMenuCategory = InTabManager->AddLocalWorkspaceMenuCategory(LOCTEXT("WorkspaceMenu_CurveTableEditor", "Curve Table Editor")); InTabManager->RegisterTabSpawner( CurveTableTabId, FOnSpawnTab::CreateSP(this, &FCurveTableEditor::SpawnTab_CurveTable) ) .SetDisplayName( LOCTEXT("CurveTableTab", "Curve Table") ) .SetGroup( WorkspaceMenuCategory.ToSharedRef() ); } void FCurveTableEditor::UnregisterTabSpawners(const TSharedRef& InTabManager) { InTabManager->UnregisterTabSpawner( CurveTableTabId ); } FCurveTableEditor::~FCurveTableEditor() { FReimportManager::Instance()->OnPostReimport().RemoveAll(this); if (UCurveTable* CurveTable = GetCurveTable()) { CurveTable->OnCurveTableChanged().RemoveAll(this); } } void FCurveTableEditor::InitCurveTableEditor( const EToolkitMode::Type Mode, const TSharedPtr< class IToolkitHost >& InitToolkitHost, UCurveTable* Table ) { const TSharedRef< FTabManager::FLayout > StandaloneDefaultLayout = InitCurveTableLayout(); FAssetEditorToolkit::InitAssetEditor( Mode, InitToolkitHost, FCurveTableEditorModule::CurveTableEditorAppIdentifier, StandaloneDefaultLayout, ShouldCreateDefaultStandaloneMenu(), ShouldCreateDefaultToolbar(), Table ); BindCommands(); ExtendMenu(); ExtendToolbar(); RegenerateMenusAndToolbars(); FReimportManager::Instance()->OnPostReimport().AddSP(this, &FCurveTableEditor::OnPostReimport); if (Table) { Table->OnCurveTableChanged().AddSP(this, &FCurveTableEditor::RefreshTableRows); } GEditor->RegisterForUndo(this); } TSharedRef< FTabManager::FLayout > FCurveTableEditor::InitCurveTableLayout() { return FTabManager::NewLayout("Standalone_CurveTableEditor_Layout_v1.1") ->AddArea ( FTabManager::NewPrimaryArea() ->Split ( FTabManager::NewStack() ->AddTab(CurveTableTabId, ETabState::OpenedTab) ->SetHideTabWell(true) ) ); } void FCurveTableEditor::BindCommands() { FCurveTableEditorCommands::Register(); ToolkitCommands->MapAction(FGenericCommands::Get().Undo, FExecuteAction::CreateLambda([]{ GEditor->UndoTransaction(); })); ToolkitCommands->MapAction(FGenericCommands::Get().Redo, FExecuteAction::CreateLambda([]{ GEditor->RedoTransaction(); })); ToolkitCommands->MapAction( FCurveTableEditorCommands::Get().CurveViewToggle, FExecuteAction::CreateSP(this, &FCurveTableEditor::ToggleViewMode), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &FCurveTableEditor::IsCurveViewChecked) ); ToolkitCommands->MapAction( FCurveTableEditorCommands::Get().AppendKeyColumn, FExecuteAction::CreateSP(this, &FCurveTableEditor::OnAddNewKeyColumn) ); ToolkitCommands->MapAction( FCurveTableEditorCommands::Get().RenameSelectedCurve, FExecuteAction::CreateSP(this, &FCurveTableEditor::OnRenameCurve) ); ToolkitCommands->MapAction( FCurveTableEditorCommands::Get().DeleteSelectedCurves, FExecuteAction::CreateSP(this, &FCurveTableEditor::OnDeleteCurves) ); } bool FCurveTableEditor::IsReadOnly() const { /* Currently, the only read-only tables are composite curve tables */ return GetCurveTable()->IsA(); } void FCurveTableEditor::ExtendMenu() { MenuExtender = MakeShareable(new FExtender); struct Local { static void ExtendMenu(FMenuBuilder& MenuBuilder) { MenuBuilder.BeginSection("CurveTableEditor", LOCTEXT("CurveTableEditor", "Curve Table")); { MenuBuilder.AddMenuEntry(FCurveTableEditorCommands::Get().CurveViewToggle); } MenuBuilder.EndSection(); } }; MenuExtender->AddMenuExtension( "WindowLayout", EExtensionHook::After, GetToolkitCommands(), FMenuExtensionDelegate::CreateStatic(&Local::ExtendMenu) ); AddMenuExtender(MenuExtender); FCurveTableEditorModule& CurveTableEditorModule = FModuleManager::LoadModuleChecked("CurveTableEditor"); AddMenuExtender(CurveTableEditorModule.GetMenuExtensibilityManager()->GetAllExtenders(GetToolkitCommands(), GetEditingObjects())); } void FCurveTableEditor::ExtendToolbar() { ToolbarExtender = MakeShareable(new FExtender); ToolbarExtender->AddToolBarExtension( "Asset", EExtensionHook::After, GetToolkitCommands(), FToolBarExtensionDelegate::CreateLambda([this](FToolBarBuilder& ParentToolbarBuilder) { ParentToolbarBuilder.BeginSection("CurveTable"); ParentToolbarBuilder.AddToolBarButton( FUIAction(FExecuteAction::CreateSP(this, &FCurveTableEditor::Reimport_Execute, GetEditingObject())), NAME_None, FText::GetEmpty(), LOCTEXT("Reimport_Tooltip", "Reimport the Curve Table from the source file. All changes will be lost. This action cannot be undone."), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "Icons.Toolbar.Reimport") ); bool HasRichCurves = GetCurveTable()->HasRichCurves(); ParentToolbarBuilder.AddWidget( SNew(SSegmentedControl) .Visibility(HasRichCurves ? EVisibility::Collapsed : EVisibility::Visible) .OnValueChanged_Lambda([this] (ECurveTableViewMode InMode) {if (InMode != GetViewMode()) ToggleViewMode(); } ) .Value(this, &FCurveTableEditor::GetViewMode) +SSegmentedControl::Slot(ECurveTableViewMode::CurveTable) .Icon(FAppStyle::Get().GetBrush("CurveTableEditor.CurveView")) +SSegmentedControl::Slot(ECurveTableViewMode::Grid) .Icon(FAppStyle::Get().GetBrush("CurveTableEditor.TableView")) ); if (!IsReadOnly()) { ParentToolbarBuilder.AddToolBarButton( FCurveTableEditorCommands::Get().AppendKeyColumn, NAME_None, FText::GetEmpty(), TAttribute(), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "Sequencer.KeyTriangle20")); } ParentToolbarBuilder.EndSection(); }) ); AddToolbarExtender(ToolbarExtender); } FName FCurveTableEditor::GetToolkitFName() const { return FName("CurveTableEditor"); } FText FCurveTableEditor::GetBaseToolkitName() const { return LOCTEXT( "AppLabel", "CurveTable Editor" ); } FString FCurveTableEditor::GetWorldCentricTabPrefix() const { return LOCTEXT("WorldCentricTabPrefix", "CurveTable ").ToString(); } FLinearColor FCurveTableEditor::GetWorldCentricTabColorScale() const { return FLinearColor( 0.0f, 0.0f, 0.2f, 0.5f ); } void FCurveTableEditor::PreChange(const UCurveTable* Changed, FCurveTableEditorUtils::ECurveTableChangeInfo Info) { } void FCurveTableEditor::PostUndo(bool bSuccess) { RefreshCachedCurveTable(); } void FCurveTableEditor::PostRedo(bool bSuccess) { RefreshCachedCurveTable(); } void FCurveTableEditor::PostChange(const UCurveTable* Changed, FCurveTableEditorUtils::ECurveTableChangeInfo Info) { const UCurveTable* Table = GetCurveTable(); if (Changed == Table) { HandlePostChange(); } } UCurveTable* FCurveTableEditor::GetCurveTable() const { return Cast(GetEditingObject()); } void FCurveTableEditor::HandlePostChange() { RefreshCachedCurveTable(); } TSharedRef FCurveTableEditor::SpawnTab_CurveTable( const FSpawnTabArgs& Args ) { check( Args.GetTabId().TabType == CurveTableTabId ); bUpdatingTableViewSelection = false; bool bTableIsReadOnly = IsReadOnly(); TSharedRef VerticalScrollBar = SNew(SScrollBar) .Orientation(Orient_Vertical); ColumnNamesHeaderRow = SNew(SHeaderRow) .Visibility(this, &FCurveTableEditor::GetTableViewControlsVisibility); CurveEditor = MakeShared(); FCurveEditorInitParams CurveEditorInitParams; CurveEditor->InitCurveEditor(CurveEditorInitParams); // We want this editor to handle undo, not the CurveEditor because // the PostUndo fixes up the selection and in the case of a CurveTable, // the curves have been rebuilt on undo and thus need special handling to restore the selection GEditor->UnregisterForUndo(CurveEditor.Get()); CurveEditorTree = SNew(SCurveEditorTree, CurveEditor.ToSharedRef()) .OnTreeViewScrolled(this, &FCurveTableEditor::OnCurveTreeViewScrolled) .OnMouseButtonDoubleClick(this, &FCurveTableEditor::OnRequestCurveRename) .OnContextMenuOpening(this, &FCurveTableEditor::OnOpenCurveMenu); TSharedRef CurveEditorPanel = SNew(SCurveEditorPanel, CurveEditor.ToSharedRef()); TableView = SNew(SListView) .IsEnabled(!bTableIsReadOnly) .ListItemsSource(&EmptyItems) .OnListViewScrolled(this, &FCurveTableEditor::OnTableViewScrolled) .HeaderRow(ColumnNamesHeaderRow) .OnGenerateRow(CurveEditorTree.Get(), &SCurveEditorTree::GenerateRow) .ExternalScrollbar(VerticalScrollBar) .SelectionMode(ESelectionMode::Multi) .OnSelectionChanged_Lambda( [this](TListTypeTraits::NullableType InItemID, ESelectInfo::Type Type) { this->OnTableViewSelectionChanged(InItemID, Type); } ); CurveEditor->GetTree()->Events.OnItemsChanged.AddSP(this, &FCurveTableEditor::RefreshTableRows); CurveEditor->GetTree()->Events.OnSelectionChanged.AddSP(this, &FCurveTableEditor::RefreshTableRowsSelection); ViewMode = GetCurveTable()->HasRichCurves() ? ECurveTableViewMode::CurveTable : ECurveTableViewMode::Grid; RefreshCachedCurveTable(); return SNew(SDockTab) .Label( LOCTEXT("CurveTableTitle", "Curve Table") ) .TabColorScale( GetTabColorScale() ) [ SNew(SBorder) .Padding(2) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ SNew(SVerticalBox) +SVerticalBox::Slot() .AutoHeight() .Padding(FMargin(8, 0)) [ MakeToolbar(CurveEditorPanel) ] +SVerticalBox::Slot() [ SNew(SSplitter) +SSplitter::Slot() .Value(.2) [ SNew(SVerticalBox) +SVerticalBox::Slot() .Padding(0, 0, 0, 1) // adjusting padding so as to line up the rows in the cell view .AutoHeight() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .Padding(2.f, 0.f, 4.f, 0.0) [ SNew(SPositiveActionButton) .Icon(FAppStyle::Get().GetBrush("Icons.Plus")) .Text(LOCTEXT("Curve", "Curve")) .OnClicked(this, &FCurveTableEditor::OnAddCurveClicked) .Visibility(bTableIsReadOnly ? EVisibility::Collapsed : EVisibility::Visible) ] +SHorizontalBox::Slot() [ SNew(SCurveEditorTreeTextFilter, CurveEditor) ] ] +SVerticalBox::Slot() [ CurveEditorTree.ToSharedRef() ] ] +SSplitter::Slot() [ SNew(SHorizontalBox) .Visibility(this, &FCurveTableEditor::GetTableViewControlsVisibility) +SHorizontalBox::Slot() [ SNew(SScrollBox) .Orientation(Orient_Horizontal) +SScrollBox::Slot() [ TableView.ToSharedRef() ] ] +SHorizontalBox::Slot() .AutoWidth() [ VerticalScrollBar ] ] +SSplitter::Slot() [ SNew(SBox) .Visibility(this, &FCurveTableEditor::GetCurveViewControlsVisibility) .IsEnabled(!bTableIsReadOnly) [ CurveEditorPanel ] ] ] ] ]; } void FCurveTableEditor::RefreshTableRows() { if (TableView && CurveEditor) { TableView->RequestListRefresh(); } } void FCurveTableEditor::RefreshTableRowsSelection() { if(bUpdatingTableViewSelection == false && TableView && CurveEditor) { TGuardValue SelectionGuard(bUpdatingTableViewSelection, true); TArray CurrentTreeWidgetSelection; TableView->GetSelectedItems(CurrentTreeWidgetSelection); const TMap& CurrentCurveEditorTreeSelection = CurveEditor->GetTreeSelection(); TArray NewTreeWidgetSelection; for (const TPair& CurveEditorTreeSelectionEntry : CurrentCurveEditorTreeSelection) { if (CurveEditorTreeSelectionEntry.Value != ECurveEditorTreeSelectionState::None) { NewTreeWidgetSelection.Add(CurveEditorTreeSelectionEntry.Key); CurrentTreeWidgetSelection.RemoveSwap(CurveEditorTreeSelectionEntry.Key); } } TableView->SetItemSelection(CurrentTreeWidgetSelection, false, ESelectInfo::Direct); TableView->SetItemSelection(NewTreeWidgetSelection, true, ESelectInfo::Direct); } } void FCurveTableEditor::OnTableViewSelectionChanged(FCurveEditorTreeItemID ItemID, ESelectInfo::Type) { if (bUpdatingTableViewSelection == false && TableView && CurveEditor) { TGuardValue SelectionGuard(bUpdatingTableViewSelection, true); CurveEditor->GetTree()->SetDirectSelection(TableView->GetSelectedItems(), CurveEditor.Get()); } } void FCurveTableEditor::RefreshCachedCurveTable() { if (!CurveEditor) { return; } // This will trigger to remove any cached widgets in the TableView while we rebuild the model from the source CurveTable const TSet& Pinned = CurveEditor->GetPinnedCurves(); TSet PinnedCurves; for (auto PinnedCurveID : Pinned) { FCurveEditorTreeItemID TreeID = CurveEditor->GetTreeIDFromCurveID(PinnedCurveID); if (RowIDMap.Contains(TreeID)) { PinnedCurves.Add(RowIDMap[TreeID]); } } TSet SelectedCurves; const TMap& Selected = CurveEditor->GetTreeSelection(); for (const TPair& SelectionEntry: Selected) { if (SelectionEntry.Value != ECurveEditorTreeSelectionState::None) { if (RowIDMap.Contains(SelectionEntry.Key)) { SelectedCurves.Add(RowIDMap[SelectionEntry.Key]); } } } // New Selection TArray NewSelectedItems; if (TableView) { TableView->SetItemsSource(&EmptyItems); } CurveEditor->RemoveAllTreeItems(); ColumnNamesHeaderRow->ClearColumns(); AvailableColumns.Empty(); RowIDMap.Empty(); UCurveTable* Table = GetCurveTable(); if (!Table || Table->GetRowMap().Num() == 0) { return; } TSharedRef FontMeasure = FSlateApplication::Get().GetRenderer()->GetFontMeasureService(); const FTextBlockStyle& CellTextStyle = FAppStyle::GetWidgetStyle("DataTableEditor.CellText"); static const float CellPadding = 10.0f; if (Table->HasRichCurves()) { InterpMode = RCIM_Cubic; for (const TPair& CurveRow : Table->GetRichCurveRowMap()) { // Setup the CurveEdtiorTree const FName& CurveName = CurveRow.Key; FCurveEditorTreeItem* TreeItem = CurveEditor->AddTreeItem(FCurveEditorTreeItemID()); TreeItem->SetStrongItem(MakeShared(SharedThis(this), TreeItem->GetID(), CurveName, FCurveTableEditorHandle(Table, CurveName), AvailableColumns)); RowIDMap.Add(TreeItem->GetID(), CurveName); if (SelectedCurves.Contains(CurveName)) { NewSelectedItems.Add(TreeItem->GetID()); } if (PinnedCurves.Contains(CurveName)) { for (auto ModelID : TreeItem->GetCurves()) { CurveEditor->PinCurve(ModelID); } } } } else { // Find unique column titles and setup columns TArray UniqueColumns; for (const TPair& CurveRow : Table->GetRowMap()) { FRealCurve* Curve = CurveRow.Value; for (auto CurveIt(Curve->GetKeyHandleIterator()); CurveIt; ++CurveIt) { UniqueColumns.AddUnique(Curve->GetKeyTime(*CurveIt)); } } UniqueColumns.Sort(); for (const float& ColumnTime : UniqueColumns) { const FText ColumnText = FText::AsNumber(ColumnTime); FCurveTableEditorColumnHeaderDataPtr CachedColumnData = MakeShareable(new FCurveTableEditorColumnHeaderData()); CachedColumnData->ColumnId = *ColumnText.ToString(); CachedColumnData->DisplayName = ColumnText; CachedColumnData->DesiredColumnWidth = FontMeasure->Measure(CachedColumnData->DisplayName, CellTextStyle.Font).X + CellPadding; CachedColumnData->KeyTime = ColumnTime; AvailableColumns.Add(CachedColumnData); ColumnNamesHeaderRow->AddColumn( GenerateHeaderColumnForKey(CachedColumnData) ); } // Setup the CurveEditorTree // Store the default Interpolation Mode InterpMode = RCIM_None; for (const TPair& CurveRow : Table->GetSimpleCurveRowMap()) { if (InterpMode == RCIM_None) { InterpMode = CurveRow.Value->GetKeyInterpMode(); } const FName& CurveName = CurveRow.Key; FCurveEditorTreeItem* TreeItem = CurveEditor->AddTreeItem(FCurveEditorTreeItemID()); TSharedPtr NewItem = MakeShared(SharedThis(this), TreeItem->GetID(), CurveName, FCurveTableEditorHandle(Table, CurveName), AvailableColumns); OnColumnsChanged.AddSP(NewItem.ToSharedRef(), &FCurveTableEditorItem::CacheKeys); TreeItem->SetStrongItem(NewItem); RowIDMap.Add(TreeItem->GetID(), CurveName); if (SelectedCurves.Contains(CurveName)) { NewSelectedItems.Add(TreeItem->GetID()); } if (PinnedCurves.Contains(CurveName)) { for (auto ModelID : TreeItem->GetOrCreateCurves(CurveEditor.Get())) { CurveEditor->PinCurve(ModelID); } } } } if (TableView) { TableView->SetItemsSource(&CurveEditorTree->GetSourceItems()); } TGuardValue SelectionGuard(bUpdatingTableViewSelection, true); CurveEditor->SetTreeSelection(MoveTemp(NewSelectedItems)); } void FCurveTableEditor::OnCurveTreeViewScrolled(double InScrollOffset) { // Synchronize the list views if (TableView) { TableView->SetScrollOffset(InScrollOffset); } } void FCurveTableEditor::OnTableViewScrolled(double InScrollOffset) { // Synchronize the list views CurveEditorTree->SetScrollOffset(InScrollOffset); } void FCurveTableEditor::OnPostReimport(UObject* InObject, bool) { const UCurveTable* Table = GetCurveTable(); if (Table && Table == InObject) { RefreshCachedCurveTable(); } } EVisibility FCurveTableEditor::GetTableViewControlsVisibility() const { return ViewMode == ECurveTableViewMode::CurveTable ? EVisibility::Collapsed : EVisibility::Visible; } EVisibility FCurveTableEditor::GetCurveViewControlsVisibility() const { return ViewMode == ECurveTableViewMode::Grid ? EVisibility::Collapsed : EVisibility::Visible; } void FCurveTableEditor::ToggleViewMode() { ViewMode = (ViewMode == ECurveTableViewMode::CurveTable) ? ECurveTableViewMode::Grid : ECurveTableViewMode::CurveTable; } bool FCurveTableEditor::IsCurveViewChecked() const { return (ViewMode == ECurveTableViewMode::CurveTable); } TSharedRef FCurveTableEditor::MakeToolbar(TSharedRef& InEditorPanel) { FSlimHorizontalToolBarBuilder ToolBarBuilder(InEditorPanel->GetCommands(), FMultiBoxCustomization::None, InEditorPanel->GetToolbarExtender(), true); ToolBarBuilder.BeginSection("Asset"); ToolBarBuilder.EndSection(); // We just use all of the extenders as our toolbar, we don't have a need to create a separate toolbar. bool bHasRichCurves = GetCurveTable()->HasRichCurves(); bool bTableIsReadOnly = IsReadOnly(); return SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(SBox) .Visibility(this, &FCurveTableEditor::GetCurveViewControlsVisibility) [ ToolBarBuilder.MakeWidget() ] ]; } FReply FCurveTableEditor::OnAddCurveClicked() { FScopedTransaction Transaction(LOCTEXT("AddCurve", "Add Curve")); UCurveTable* Table = Cast(GetEditingObject()); check(Table != nullptr); Table->Modify(); if (Table->HasRichCurves()) { FName NewCurveUnique = MakeUniqueCurveName(Table); FRichCurve& NewCurve = Table->AddRichCurve(NewCurveUnique); FCurveEditorTreeItem* TreeItem = CurveEditor->AddTreeItem(FCurveEditorTreeItemID()); TreeItem->SetStrongItem(MakeShared(SharedThis(this), TreeItem->GetID(), NewCurveUnique, FCurveTableEditorHandle(Table, NewCurveUnique), AvailableColumns)); RowIDMap.Add(TreeItem->GetID(), NewCurveUnique); } else { FName NewCurveUnique = MakeUniqueCurveName(Table); FSimpleCurve& RealCurve = Table->AddSimpleCurve(NewCurveUnique); RealCurve.SetKeyInterpMode(InterpMode); // Also add a default key for each column for (auto Column : AvailableColumns) { RealCurve.AddKey(Column->KeyTime, 0.0); } FCurveEditorTreeItem* TreeItem = CurveEditor->AddTreeItem(FCurveEditorTreeItemID()); TSharedPtr NewItem = MakeShared(SharedThis(this), TreeItem->GetID(), NewCurveUnique, FCurveTableEditorHandle(Table, NewCurveUnique), AvailableColumns); OnColumnsChanged.AddSP(NewItem.ToSharedRef(), &FCurveTableEditorItem::CacheKeys); TreeItem->SetStrongItem(NewItem); RowIDMap.Add(TreeItem->GetID(), NewCurveUnique); } return FReply::Handled(); } void FCurveTableEditor::OnAddNewKeyColumn() { UCurveTable* Table = Cast(GetEditingObject()); check(Table != nullptr); if (!Table->HasRichCurves()) { // Compute a new keytime based on the last columns float NewKeyTime = 1.0; if (AvailableColumns.Num() > 1) { float LastKeyTime = AvailableColumns[AvailableColumns.Num() - 1]->KeyTime; float PrevKeyTime = AvailableColumns[AvailableColumns.Num() - 2]->KeyTime; NewKeyTime = 2.*LastKeyTime - PrevKeyTime; } else if (AvailableColumns.Num() > 0) { float LastKeyTime = AvailableColumns[AvailableColumns.Num() - 1]->KeyTime; NewKeyTime = LastKeyTime + 1; } AddNewKeyColumn(NewKeyTime); } } void FCurveTableEditor::AddNewKeyColumn(float NewKeyTime) { UCurveTable* Table = Cast(GetEditingObject()); check(Table != nullptr); if (!Table->HasRichCurves()) { FScopedTransaction Transaction(LOCTEXT("AddKeyColumn", "AddKeyColumn")); Table->Modify(); // Make sure we don't already have a key at this time // 1. Add new keys to every curve for (const TPair& CurveRow : Table->GetRowMap()) { FRealCurve* Curve = CurveRow.Value; Curve->UpdateOrAddKey(NewKeyTime, Curve->Eval(NewKeyTime)); } // 2. Add Column to our Table FCurveTableEditorColumnHeaderDataPtr ColumnData = MakeShareable(new FCurveTableEditorColumnHeaderData()); const FText ColumnText = FText::AsNumber(NewKeyTime); ColumnData->ColumnId = *ColumnText.ToString(); ColumnData->DisplayName = ColumnText; ColumnData->KeyTime = NewKeyTime; TSharedRef FontMeasure = FSlateApplication::Get().GetRenderer()->GetFontMeasureService(); const FTextBlockStyle& CellTextStyle = FAppStyle::GetWidgetStyle("DataTableEditor.CellText"); ColumnData->DesiredColumnWidth = FontMeasure->Measure(ColumnData->DisplayName, CellTextStyle.Font).X + 10.f; AvailableColumns.Add(ColumnData); // 3. Let the CurveTreeItems know they need to recache OnColumnsChanged.Broadcast(); ColumnNamesHeaderRow->AddColumn( GenerateHeaderColumnForKey(ColumnData) ); } } void FCurveTableEditor::OnRequestCurveRename(FCurveEditorTreeItemID TreeItemId) { const FCurveEditorTreeItem* TreeItem = CurveEditor->FindTreeItem(TreeItemId); if (TreeItem != nullptr) { TSharedPtr CurveEditorTreeItem = TreeItem->GetItem(); if (CurveEditorTreeItem.IsValid()) { TSharedPtr CurveTableEditorItem = StaticCastSharedPtr(CurveEditorTreeItem); CurveTableEditorItem->EnterRenameMode(); } } } void FCurveTableEditor::HandleCurveRename(FCurveEditorTreeItemID& TreeID, FName& CurrentCurve, FName& NewCurveName) { // Update the underlying Curve Data Asset itself UCurveTable* Table = Cast(GetEditingObject()); check(Table != nullptr); FScopedTransaction Transaction(LOCTEXT("RenameCurve", "Rename Curve")); Table->SetFlags(RF_Transactional); Table->Modify(); Table->RenameRow(CurrentCurve, NewCurveName); FPropertyChangedEvent PropertyChangeStruct(nullptr, EPropertyChangeType::ValueSet); Table->PostEditChangeProperty(PropertyChangeStruct); // Update our internal map of TreeIDs to FNames RowIDMap[TreeID] = NewCurveName; } void FCurveTableEditor::OnRenameCurve() { const TMap& SelectedRows = CurveEditor->GetTreeSelection(); if (SelectedRows.Num() == 1) { for (auto Item : SelectedRows) { OnRequestCurveRename(Item.Key); } } } void FCurveTableEditor::OnDeleteCurves() { UCurveTable* Table = Cast(GetEditingObject()); check(Table != nullptr); const TMap& SelectedRows = CurveEditor->GetTreeSelection(); if (SelectedRows.Num() >= 1) { FScopedTransaction Transaction(LOCTEXT("DeleteCurveRow", "Delete Curve Rows")); Table->SetFlags(RF_Transactional); Table->Modify(); for (auto Item : SelectedRows) { CurveEditor->RemoveTreeItem(Item.Key); FName& CurveName = RowIDMap[Item.Key]; Table->DeleteRow(CurveName); RowIDMap.Remove(Item.Key); } FPropertyChangedEvent PropertyChangeStruct(nullptr, EPropertyChangeType::ValueSet); Table->PostEditChangeProperty(PropertyChangeStruct); } } TSharedPtr FCurveTableEditor::OnOpenCurveMenu() { int32 SelectedRowCount = CurveEditor->GetTreeSelection().Num(); if (SelectedRowCount > 0 && !IsReadOnly()) { FMenuBuilder MenuBuilder(true /*auto close*/, ToolkitCommands); MenuBuilder.BeginSection("Edit"); if (SelectedRowCount == 1) { MenuBuilder.AddMenuEntry( FCurveTableEditorCommands::Get().RenameSelectedCurve, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Edit") ); } MenuBuilder.AddMenuEntry( FCurveTableEditorCommands::Get().DeleteSelectedCurves, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Delete") ); MenuBuilder.EndSection(); return MenuBuilder.MakeWidget(); } return SNullWidget::NullWidget; } void FCurveTableEditor::OnDeleteKeyColumn(float KeyTime) { UCurveTable* Table = Cast(GetEditingObject()); check(Table != nullptr); if (!Table->HasRichCurves()) { // First find the column data associated with the original keytime int FoundIndex = -1; for (int i = 0; i < AvailableColumns.Num(); i++) { if (FMath::IsNearlyEqual(KeyTime, AvailableColumns[i]->KeyTime, KINDA_SMALL_NUMBER)) { FoundIndex = i; break; } } if (FoundIndex < 0) { return; } FCurveTableEditorColumnHeaderDataPtr ColumnData = AvailableColumns[FoundIndex]; if (ColumnData.IsValid()) { // Remove the column from the ui AvailableColumns.RemoveAt(FoundIndex); ColumnNamesHeaderRow->RemoveColumn(ColumnData->ColumnId); // Remove the keys from all curve rows is the data table FScopedTransaction Transaction(LOCTEXT("DeleteKeyColumn", "Delete Key Column")); Table->Modify(); for (const TPair& CurveRow : Table->GetRowMap()) { FRealCurve* Curve = CurveRow.Value; FKeyHandle KeyHandle = Curve->FindKey(KeyTime); if (KeyHandle != FKeyHandle::Invalid()) { Curve->DeleteKey(KeyHandle); } } FPropertyChangedEvent PropertyChangeStruct(nullptr, EPropertyChangeType::ValueSet); Table->PostEditChangeProperty(PropertyChangeStruct); // Let the CurveTreeItems (row ui) know they need to recache OnColumnsChanged.Broadcast(); } } } bool FCurveTableEditor::VerifyValidRetime(const FText& InText, FText& OutErrorMessage, float OriginalTime) { if (!InText.IsNumeric()) { OutErrorMessage = LOCTEXT("KeysMustBeNumeric", "Key Times must be numeric."); return false; } float NewTime = 0.0f; LexFromString(NewTime, *InText.ToString()); // do we already have a column with this time? for (auto Col : AvailableColumns) { if (FMath::IsNearlyEqual(NewTime, Col->KeyTime, KINDA_SMALL_NUMBER)) { OutErrorMessage = LOCTEXT("KeyAlreadyExists", "Key times must be unique!"); return false; } } return true; } void FCurveTableEditor::HandleRetimeCommitted(const FText& InText, ETextCommit::Type CommitInfo, float OriginalKeyTime) { // First find the column data associated with the original keytime int FoundIndex = -1; for (int i = 0; i < AvailableColumns.Num(); i++) { if (FMath::IsNearlyEqual(OriginalKeyTime, AvailableColumns[i]->KeyTime, KINDA_SMALL_NUMBER)) { FoundIndex = i; break; } } if (FoundIndex < 0) { return; } FCurveTableEditorColumnHeaderDataPtr CachedColumnData = AvailableColumns[FoundIndex]; if (CachedColumnData.IsValid()) { // 1. Remove the UI associated with this column (ColumnData and the SHeaderRow::FColumn) AvailableColumns.RemoveAt(FoundIndex); ColumnNamesHeaderRow->RemoveColumn(CachedColumnData->ColumnId); float NewTime = 0.0f; LexFromString(NewTime, *InText.ToString()); // 2. Adjust the key times for each of the curve table rows UCurveTable* Table = Cast(GetEditingObject()); check(Table != nullptr); FScopedTransaction Transaction(LOCTEXT("RetimeKeyColumn", "Retime Key Column")); Table->Modify(); for (const TPair& CurveRow : Table->GetRowMap()) { FRealCurve* Curve = CurveRow.Value; FKeyHandle KeyHandle = Curve->FindKey(OriginalKeyTime); if (KeyHandle != FKeyHandle::Invalid()) { Curve->SetKeyTime(KeyHandle, NewTime); } } FPropertyChangedEvent PropertyChangeStruct(nullptr, EPropertyChangeType::ValueSet); Table->PostEditChangeProperty(PropertyChangeStruct); // 3. Update the ColumnData and re-insert the ColumnData and SHeaderRow::FColumn into the // correct places in order of the key times int NewIndex = 0; while (NewIndex < AvailableColumns.Num() && NewTime > AvailableColumns[NewIndex]->KeyTime ) { NewIndex++; } const FText ColumnText = FText::AsNumber(NewTime); CachedColumnData->ColumnId = *ColumnText.ToString(); CachedColumnData->DisplayName = ColumnText; CachedColumnData->KeyTime = NewTime; AvailableColumns.Insert(CachedColumnData, NewIndex); // Let the CurveTreeItems know they need to recache // note we do this before adding the column to the header so the rows already have their // data in place and are prepared to draw OnColumnsChanged.Broadcast(); ColumnNamesHeaderRow->InsertColumn( GenerateHeaderColumnForKey(CachedColumnData), NewIndex) ; } } SHeaderRow::FColumn::FArguments FCurveTableEditor::GenerateHeaderColumnForKey(FCurveTableEditorColumnHeaderDataPtr ColumnData) { TSharedRef KeyTimeWidget = SNew(SInlineEditableTextBlock) .Text(ColumnData->DisplayName) .Justification(ETextJustify::Center) .ColorAndOpacity(FSlateColor::UseForeground()) .OnTextCommitted(this, &FCurveTableEditor::HandleRetimeCommitted, ColumnData->KeyTime) .OnVerifyTextChanged(this, &FCurveTableEditor::VerifyValidRetime, ColumnData->KeyTime); // Create the Column Header's R-Click Menu FMenuBuilder MenuBuilder(true /*Auto close*/, ToolkitCommands); MenuBuilder.BeginSection("Edit"); MenuBuilder.AddMenuEntry( FText::Format(LOCTEXT("RetimeKeysColumn", "Retime Keys at {0}"), FText::AsNumber(ColumnData->KeyTime)), FText::Format(LOCTEXT("RetimeKeysColumn_Tooltip", "Retimes this column and all keys at {0}"), FText::AsNumber(ColumnData->KeyTime)), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Edit"), FUIAction(FExecuteAction::CreateSP(KeyTimeWidget, &SInlineEditableTextBlock::EnterEditingMode)) ); MenuBuilder.AddMenuEntry( FText::Format(LOCTEXT("DeleteKeysColumn", "Delete Keys at {0}"), FText::AsNumber(ColumnData->KeyTime)), FText::Format(LOCTEXT("DeleteKeysColumn_Tooltip", "Deletes this column and all keys at {0}"), FText::AsNumber(ColumnData->KeyTime)), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Delete"), FUIAction(FExecuteAction::CreateSP(this, &FCurveTableEditor::OnDeleteKeyColumn, ColumnData->KeyTime)) ); MenuBuilder.EndSection(); return SHeaderRow::Column(ColumnData->ColumnId) .DefaultLabel(ColumnData->DisplayName) .FixedWidth(ColumnData->DesiredColumnWidth + 40) .HAlignHeader(HAlign_Fill) .MenuContent() [ MenuBuilder.MakeWidget() ] .HeaderContent() [ SNew(SBox) .HeightOverride(22.f) .HAlign(HAlign_Fill) .VAlign(VAlign_Fill) [ KeyTimeWidget ] ]; } #undef LOCTEXT_NAMESPACE