// Copyright Epic Games, Inc. All Rights Reserved. #include "CurveTableEditor.h" #include "Widgets/Text/STextBlock.h" #include "Modules/ModuleManager.h" #include "Fonts/FontMeasure.h" #include "Framework/Commands/GenericCommands.h" #include "Framework/Application/SlateApplication.h" #include "Widgets/Layout/SScrollBar.h" #include "Framework/Layout/Overscroll.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SNumericEntryBox.h" #include "Widgets/Input/SSegmentedControl.h" #include "Widgets/Views/SListView.h" #include "Widgets/Layout/SScrollBox.h" #include "SPositiveActionButton.h" #include "EditorStyleSet.h" #include "Styling/StyleColors.h" #include "EditorReimportHandler.h" #include "CurveTableEditorModule.h" #include "Widgets/Docking/SDockTab.h" #include "CurveEditor.h" #include "SCurveEditorPanel.h" #include "Tree/SCurveEditorTree.h" #include "Tree/SCurveEditorTreeSelect.h" #include "Tree/SCurveEditorTreePin.h" #include "Tree/ICurveEditorTreeItem.h" #include "Tree/CurveEditorTreeFilter.h" #include "Tree/SCurveEditorTreeTextFilter.h" #include "RealCurveModel.h" #include "RichCurveEditorModel.h" #include "CurveTableEditorCommands.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #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 (const FName& InRowId, FCurveTableEditorHandle InRowHandle, const TArray& InColumns) : 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() [ SNew(STextBlock) .Text(DisplayName) .ColorAndOpacity(FSlateColor::UseForeground()) ]; } 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([Curve, KeyHandle] () { return Curve->GetKeyValue(KeyHandle); }) .OnValueChanged_Lambda([this, KeyHandle] (float NewValue) { FScopedTransaction Transaction(LOCTEXT("SetKeyValues", "Set Key Values")); RowHandle.ModifyOwner(); RowHandle.GetCurve()->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) { const FCurveEditorTreeTextFilter* Filter = static_cast(InFilter); for (const FCurveEditorTreeTextFilterTerm& Term : Filter->GetTerms()) { for(const FCurveEditorTreeTextFilterToken& Token : Term.ChildToParentTokens) { if(Token.Match(*DisplayName.ToString())) { return true; } } } return false; } 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))); } } } } /** 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; }; 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); } 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(); RegenerateMenusAndToolbars(); FReimportManager::Instance()->OnPostReimport().AddSP(this, &FCurveTableEditor::OnPostReimport); 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)); } 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())); } 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; 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); TSharedRef CurveEditorPanel = SNew(SCurveEditorPanel, CurveEditor.ToSharedRef()); TableView = SNew(SListView) .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(FEditorStyle::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) ] +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) [ CurveEditorPanel ] ] ] ] ]; } void FCurveTableEditor::RefreshTableRows() { TableView->RequestListRefresh(); } void FCurveTableEditor::RefreshTableRowsSelection() { if(bUpdatingTableViewSelection == false) { 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) { TGuardValue SelectionGuard(bUpdatingTableViewSelection, true); CurveEditor->GetTree()->SetDirectSelection(TableView->GetSelectedItems(), CurveEditor.Get()); } } void FCurveTableEditor::RefreshCachedCurveTable() { // 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; TableView->SetListItemsSource(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 = FEditorStyle::GetWidgetStyle("DataTableEditor.CellText"); static const float CellPadding = 10.0f; if (Table->HasRichCurves()) { for (const TPair& CurveRow : Table->GetRichCurveRowMap()) { // Setup the CurveEdtiorTree const FName& CurveName = CurveRow.Key; FCurveEditorTreeItem* TreeItem = CurveEditor->AddTreeItem(FCurveEditorTreeItemID()); TreeItem->SetStrongItem(MakeShared(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( SHeaderRow::Column(CachedColumnData->ColumnId) .DefaultLabel(CachedColumnData->DisplayName) .FixedWidth(CachedColumnData->DesiredColumnWidth + 50) .HAlignHeader(HAlign_Center) ); } // 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(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); } } } } TableView->SetListItemsSource(CurveEditorTree->GetSourceItems()); TGuardValue SelectionGuard(bUpdatingTableViewSelection, true); CurveEditor->SetTreeSelection(MoveTemp(NewSelectedItems)); } void FCurveTableEditor::OnCurveTreeViewScrolled(double InScrollOffset) { // Synchronize the list views 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) { FToolBarBuilder ToolBarBuilder(InEditorPanel->GetCommands(), FMultiBoxCustomization::None, InEditorPanel->GetToolbarExtender(), true); ToolBarBuilder.SetStyle(&FAppStyle::Get(), "Sequencer.ToolBar"); 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 HasRichCurves = GetCurveTable()->HasRichCurves(); return SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .HAlign(HAlign_Center) .Padding(FMargin(2.0, 4.0, 8.f, 4.f)) [ 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")) ] +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(SBox) [ SNew(SButton) .ButtonStyle( &FAppStyle::Get().GetWidgetStyle< FButtonStyle >( "SimpleButton" ) ) .Visibility(this, &FCurveTableEditor::GetTableViewControlsVisibility) .OnClicked(this, &FCurveTableEditor::OnAddNewKeyColumn) .ToolTipText(LOCTEXT("CurveTableEditor_AddKeyColumnTooltip", "Append a new column to the curve table.\nEvery Curve or Table Row will have a new key appended.")) [ SNew(SImage) .ColorAndOpacity(FSlateColor::UseForeground()) .Image(FAppStyle::Get().GetBrush("Sequencer.KeyTriangle")) ] ] ] +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(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(NewCurveUnique, FCurveTableEditorHandle(Table, NewCurveUnique), AvailableColumns); OnColumnsChanged.AddSP(NewItem.ToSharedRef(), &FCurveTableEditorItem::CacheKeys); TreeItem->SetStrongItem(NewItem); RowIDMap.Add(TreeItem->GetID(), NewCurveUnique); } return FReply::Handled(); } FReply 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); } return FReply::Handled(); } 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->DesiredColumnWidth = FontMeasure->Measure(ColumnData->DisplayName, CellTextStyle.Font).X + CellPadding; ColumnData->KeyTime = NewKeyTime; AvailableColumns.Add(ColumnData); // 3. Let the CurveTreeItems know they need to recache OnColumnsChanged.Broadcast(); // Add the column to the TableView Header Row ColumnNamesHeaderRow->AddColumn( SHeaderRow::Column(ColumnData->ColumnId) .DefaultLabel(ColumnData->DisplayName) .FixedWidth(ColumnData->DesiredColumnWidth + 50) .HAlignHeader(HAlign_Center) ); } } #undef LOCTEXT_NAMESPACE