// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. #include "PersonaPrivatePCH.h" #include "SRigWindow.h" #include "ObjectTools.h" #include "ScopedTransaction.h" #include "AssetRegistryModule.h" #include "Editor/PropertyEditor/Public/PropertyEditorModule.h" #include "Editor/ContentBrowser/Public/ContentBrowserModule.h" #include "WorkflowOrientedApp/SContentReference.h" #include "AssetNotifications.h" #include "Animation/Rig.h" #include "BoneSelectionWidget.h" #include "SSearchBox.h" #include "SInlineEditableTextBlock.h" #include "SRigPicker.h" #define LOCTEXT_NAMESPACE "SRigWindow" static const FName ColumnId_NodeNameLabel( "Node Name" ); static const FName ColumnID_BoneNameLabel( "Bone" ); DECLARE_DELEGATE_TwoParams(FOnBoneMappingChanged, FName /** NodeName */, FName /** BoneName **/); DECLARE_DELEGATE_RetVal_OneParam(FName, FOnGetBoneMapping, FName /** Node Name **/); ////////////////////////////////////////////////////////////////////////// // SBoneMappingListRow typedef TSharedPtr< FDisplayedBoneMappingInfo > FDisplayedBoneMappingInfoPtr; class SBoneMappingListRow : public SMultiColumnTableRow< FDisplayedBoneMappingInfoPtr > { public: SLATE_BEGIN_ARGS( SBoneMappingListRow ) {} /** The item for this row **/ SLATE_ARGUMENT( FDisplayedBoneMappingInfoPtr, Item ) /* The SRigWindow that handles all retarget sources */ SLATE_ARGUMENT( class SRigWindow*, RigWindow ) /* Widget used to display the list of retarget sources*/ SLATE_ARGUMENT( TSharedPtr, BoneMappingListView ) /* Persona used to update the viewport when a weight slider is dragged */ SLATE_ARGUMENT( TWeakPtr, Persona ) SLATE_EVENT( FOnBoneMappingChanged, OnBoneMappingChanged) SLATE_EVENT( FOnGetBoneMapping, OnGetBoneMapping) SLATE_END_ARGS() void Construct( const FArguments& InArgs, const TSharedRef& OwnerTableView ); /** Overridden from SMultiColumnTableRow. Generates a widget for this column of the tree row. */ virtual TSharedRef GenerateWidgetForColumn( const FName& ColumnName ) override; private: /* The SRigWindow that handles all retarget sources*/ // @todo remove SRigWindow* RigWindow; /** Widget used to display the list of retarget sources*/ TSharedPtr BoneMappingListView; /** The name and weight of the retarget source*/ FDisplayedBoneMappingInfoPtr Item; /** Pointer back to the Persona that owns us */ TWeakPtr PersonaPtr; // Bone tree widget delegates void OnBoneSelectionChanged(FName Name); FReply OnClearButtonClicked(); FName GetSelectedBone() const; FOnBoneMappingChanged OnBoneMappingChanged; FOnGetBoneMapping OnGetBoneMapping; }; void SBoneMappingListRow::Construct( const FArguments& InArgs, const TSharedRef& InOwnerTableView ) { Item = InArgs._Item; RigWindow = InArgs._RigWindow; BoneMappingListView = InArgs._BoneMappingListView; OnBoneMappingChanged = InArgs._OnBoneMappingChanged; OnGetBoneMapping = InArgs._OnGetBoneMapping; PersonaPtr = InArgs._Persona; check( Item.IsValid() ); SMultiColumnTableRow< FDisplayedBoneMappingInfoPtr >::Construct( FSuperRowType::FArguments(), InOwnerTableView ); } TSharedRef< SWidget > SBoneMappingListRow::GenerateWidgetForColumn( const FName& ColumnName ) { if ( ColumnName == ColumnId_NodeNameLabel ) { TSharedPtr< SInlineEditableTextBlock > InlineWidget; TSharedRef< SWidget > NewWidget = SNew( SVerticalBox ) + SVerticalBox::Slot() .AutoHeight() .Padding( 0.0f, 4.0f ) .VAlign( VAlign_Center ) [ SAssignNew(InlineWidget, SInlineEditableTextBlock) .Text( FText::FromString(Item->GetDisplayName()) ) .HighlightText( RigWindow->GetFilterText() ) .IsReadOnly(true) .IsSelected(this, &SMultiColumnTableRow< FDisplayedBoneMappingInfoPtr >::IsSelectedExclusively) ]; return NewWidget; } else { check (Item->Skeleton); // show bone list // Encase the SSpinbox in an SVertical box so we can apply padding. Setting ItemHeight on the containing SListView has no effect :-( return SNew( SVerticalBox ) + SVerticalBox::Slot() .AutoHeight() .Padding( 0.0f, 1.0f ) .VAlign( VAlign_Center ) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() [ SNew(SBoneSelectionWidget) .Skeleton(Item->Skeleton) .Tooltip(FText::Format(LOCTEXT("BoneSelectinWidget", "Select Bone for node {0}"), FText::FromString(Item->GetDisplayName()))) .OnBoneSelectionChanged(this, &SBoneMappingListRow::OnBoneSelectionChanged) .OnGetSelectedBone(this, &SBoneMappingListRow::GetSelectedBone) ] +SHorizontalBox::Slot() .AutoWidth() [ SNew(SButton) .OnClicked(FOnClicked::CreateSP(this, &SBoneMappingListRow::OnClearButtonClicked)) .Text(FText::FromString(TEXT("x"))) ] ]; } } FReply SBoneMappingListRow::OnClearButtonClicked() { if(OnBoneMappingChanged.IsBound()) { OnBoneMappingChanged.Execute(Item->GetNodeName(), NAME_None); } return FReply::Handled(); } void SBoneMappingListRow::OnBoneSelectionChanged(FName Name) { if (OnBoneMappingChanged.IsBound()) { OnBoneMappingChanged.Execute(Item->GetNodeName(), Name); } } FName SBoneMappingListRow::GetSelectedBone() const { if (OnGetBoneMapping.IsBound()) { return OnGetBoneMapping.Execute(Item->GetNodeName()); } // @todo delete? // return Item->BoneName return NAME_None; } ////////////////////////////////////////////////////////////////////////// // SRigWindow void SRigWindow::Construct(const FArguments& InArgs) { PersonaPtr = InArgs._Persona; Skeleton = NULL; bDisplayAdvanced = false; if ( PersonaPtr.IsValid() ) { Skeleton = PersonaPtr.Pin()->GetSkeleton(); PersonaPtr.Pin()->RegisterOnPostUndo(FPersona::FOnPostUndo::CreateSP( this, &SRigWindow::PostUndo ) ); } // @todo it will crash right nwo without Skeleton check (Skeleton); Skeleton->RefreshRigConfig(); // show list of skeletalmeshes that they can choose from FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked(TEXT("ContentBrowser")); ChildSlot [ SNew( SVerticalBox ) // first add rig asset picker + SVerticalBox::Slot() .AutoHeight() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() [ SNew(STextBlock) .Text(LOCTEXT("RigNameLabel", "Select Rig ")) .Font(FEditorStyle::GetFontStyle("Persona.RetargetManager.BoldFont")) ] +SHorizontalBox::Slot() [ SAssignNew( AssetComboButton, SComboButton ) //.ToolTipText( this, &SPropertyEditorAsset::OnGetToolTip ) .ButtonStyle( FEditorStyle::Get(), "PropertyEditor.AssetComboStyle" ) .ForegroundColor(FEditorStyle::GetColor("PropertyEditor.AssetName.ColorAndOpacity")) .OnGetMenuContent( this, &SRigWindow::MakeRigPickerWithMenu ) .ContentPadding(2.0f) .ButtonContent() [ // Show the name of the asset or actor SNew(STextBlock) .TextStyle( FEditorStyle::Get(), "PropertyEditor.AssetClass" ) .Font( FEditorStyle::GetFontStyle( "PropertyWindow.NormalFont" ) ) .Text(this,&SRigWindow::GetAssetName) ] ] +SHorizontalBox::Slot() .HAlign(HAlign_Right) [ SNew(SButton) .OnClicked(FOnClicked::CreateSP(this, &SRigWindow::OnToggleAdvanced)) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .Text(this, &SRigWindow::GetAdvancedButtonText) .ToolTipText(LOCTEXT("ToggleAdvanced_Tooltip", "Toggle Base/Advanced configuration")) ] ] // now show bone mapping + SVerticalBox::Slot() .AutoHeight() .Padding(0,2) [ SNew(SHorizontalBox) // Filter entry +SHorizontalBox::Slot() .FillWidth( 1 ) [ SAssignNew( NameFilterBox, SSearchBox ) .SelectAllTextWhenFocused( true ) .OnTextChanged( this, &SRigWindow::OnFilterTextChanged ) .OnTextCommitted( this, &SRigWindow::OnFilterTextCommitted ) ] ] + SVerticalBox::Slot() .FillHeight( 1.0f ) // This is required to make the scrollbar work, as content overflows Slate containers by default [ SAssignNew( BoneMappingListView, SBoneMappingListType ) .ListItemsSource( &BoneMappingList ) .OnGenerateRow( this, &SRigWindow::GenerateBoneMappingRow ) .ItemHeight( 22.0f ) .HeaderRow ( SNew( SHeaderRow ) + SHeaderRow::Column( ColumnId_NodeNameLabel ) .DefaultLabel( LOCTEXT( "RigWindow_NodeNameLabel", "Node (Rig)" ) ) .FixedWidth(150.f) + SHeaderRow::Column( ColumnID_BoneNameLabel ) .DefaultLabel( LOCTEXT( "RigWindow_BoneNameLabel", "Bone (Skeleton)" ) ) ) ] ]; CreateBoneMappingList(); } void SRigWindow::OnFilterTextChanged( const FText& SearchText ) { // need to make sure not to have the same text go // otherwise, the widget gets recreated multiple times causing // other issue if (FilterText.CompareToCaseIgnored(SearchText) != 0) { FilterText = SearchText; CreateBoneMappingList(SearchText.ToString()); } } void SRigWindow::OnFilterTextCommitted( const FText& SearchText, ETextCommit::Type CommitInfo ) { // Just do the same as if the user typed in the box OnFilterTextChanged( SearchText ); } TSharedRef SRigWindow::GenerateBoneMappingRow(TSharedPtr InInfo, const TSharedRef& OwnerTable) { check( InInfo.IsValid() ); return SNew( SBoneMappingListRow, OwnerTable ) .Persona( PersonaPtr ) .Item( InInfo ) .RigWindow( this ) .BoneMappingListView( BoneMappingListView ) .OnBoneMappingChanged(this, &SRigWindow::OnBoneMappingChanged) .OnGetBoneMapping(this, &SRigWindow::GetBoneMapping); } void SRigWindow::CreateBoneMappingList( const FString& SearchText) { BoneMappingList.Empty(); const URig* Rig = Skeleton->GetRig(); if ( Rig ) { bool bDoFiltering = !SearchText.IsEmpty(); const TArray& Nodes = Rig->GetNodes(); for ( const auto Node : Nodes ) { const FName& Name = Node.Name; const FString& DisplayName = Node.DisplayName; const FName& BoneName = Skeleton->GetRigBoneMapping(Name); if (Node.bAdvanced == bDisplayAdvanced) { if(bDoFiltering) { // make sure it doens't fit any of them if(!Name.ToString().Contains(SearchText) && !DisplayName.Contains(SearchText) && !BoneName.ToString().Contains(SearchText)) { continue; // Skip items that don't match our filter } } TSharedRef Info = FDisplayedBoneMappingInfo::Make(Name, DisplayName, Skeleton); BoneMappingList.Add(Info); } } } BoneMappingListView->RequestListRefresh(); } void SRigWindow::OnAssetSelected(UObject* Object) { if (Skeleton) { AssetComboButton->SetIsOpen(false); const FScopedTransaction Transaction(LOCTEXT("RigAssetChanged", "Select Rig")); Skeleton->Modify(); Skeleton->SetRigConfig(Cast(Object)); CreateBoneMappingList(FilterText.ToString()); FAssetNotifications::SkeletonNeedsToBeSaved(Skeleton); } } /** Returns true if the asset shouldn't show */ bool SRigWindow::ShouldFilterAsset(const class FAssetData& AssetData) { return (AssetData.GetAsset() == GetRigObject()); } UObject* SRigWindow::GetRigObject() const { return (Skeleton)? Skeleton->GetRig() : NULL; } SRigWindow::~SRigWindow() { if (PersonaPtr.IsValid()) { PersonaPtr.Pin()->UnregisterOnPostUndo(this); } } void SRigWindow::PostUndo() { CreateBoneMappingList(FilterText.ToString()); } void SRigWindow::OnBoneMappingChanged(FName NodeName, FName BoneName) { const FScopedTransaction Transaction(LOCTEXT("BoneMappingChanged", "Change Bone Mapping")); Skeleton->Modify(); Skeleton->SetRigBoneMapping(NodeName, BoneName); } FName SRigWindow::GetBoneMapping(FName NodeName) { return Skeleton->GetRigBoneMapping(NodeName); } FReply SRigWindow::OnToggleAdvanced() { bDisplayAdvanced = !bDisplayAdvanced; CreateBoneMappingList(FilterText.ToString()); return FReply::Handled(); } FText SRigWindow::GetAdvancedButtonText() const { if (bDisplayAdvanced) { return LOCTEXT("ShowBase", "Show Base"); } return LOCTEXT("ShowAdvanced", "Show Advanced"); } TSharedRef SRigWindow::MakeRigPickerWithMenu() { // rig asset picker return SNew(SRigPicker) .InitialObject(Skeleton->GetRig()) .OnShouldFilterAsset(this, &SRigWindow::ShouldFilterAsset) .OnSetReference(this, &SRigWindow::OnAssetSelected) .OnClose(this, &SRigWindow::CloseComboButton ); } void SRigWindow::CloseComboButton() { AssetComboButton->SetIsOpen(false); } FText SRigWindow::GetAssetName() const { UObject* Rig = GetRigObject(); if (Rig) { return FText::FromString(Rig->GetName()); } return LOCTEXT("None", "None"); } #undef LOCTEXT_NAMESPACE