// Copyright 2019-Present LexLiu. All Rights Reserved. #include "LGUIPrefabSequenceEditor.h" #include "PrefabAnimation/LGUIPrefabSequence.h" #include "PrefabAnimation/LGUIPrefabSequenceComponent.h" #include "EditorStyleSet.h" #include "GameFramework/Actor.h" #include "IDetailsView.h" #include "DetailLayoutBuilder.h" #include "DetailCategoryBuilder.h" #include "DetailWidgetRow.h" #include "Widgets/Docking/SDockTab.h" #include "SSCSEditor.h" #include "BlueprintEditorTabs.h" #include "ScopedTransaction.h" #include "ISequencerModule.h" #include "Editor.h" #include "LGUIPrefabSequenceEditorWidget.h" #include "IPropertyUtilities.h" #include "Widgets/Input/SButton.h" #include "Widgets/Views/SListView.h" #include "Widgets/Layout/SScrollBorder.h" #include "Widgets/Input/SSearchBox.h" #include "LGUIEditorModule.h" #include "Framework/Commands/GenericCommands.h" #include "Widgets/Text/SInlineEditableTextBlock.h" #include "Misc/TextFilter.h" #include "PropertyCustomizationHelpers.h" #include "PrefabSystem/LGUIPrefabHelperObject.h" #include "LGUIEditorTools.h" #define LOCTEXT_NAMESPACE "SLGUIPrefabSequenceEditor" struct FWidgetAnimationListItem { FWidgetAnimationListItem(ULGUIPrefabSequence* InAnimation, bool bInRenameRequestPending = false, bool bInNewAnimation = false) : Animation(InAnimation) , bRenameRequestPending(bInRenameRequestPending) , bNewAnimation(bInNewAnimation) {} ULGUIPrefabSequence* Animation; bool bRenameRequestPending; bool bNewAnimation; }; typedef SListView > SWidgetAnimationListView; class SWidgetAnimationListItem : public STableRow > { public: SLATE_BEGIN_ARGS(SWidgetAnimationListItem) {} SLATE_END_ARGS() void Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView, SLGUIPrefabSequenceEditor* InEditor, TSharedPtr InListItem) { ListItem = InListItem; Editor = InEditor; STableRow>::Construct( STableRow>::FArguments() .Padding(FMargin(3.0f, 2.0f)) .Content() [ SAssignNew(InlineTextBlock, SInlineEditableTextBlock) .Font(FCoreStyle::Get().GetFontStyle("NormalFont")) .Text(this, &SWidgetAnimationListItem::GetMovieSceneText) //.HighlightText(InArgs._HighlightText) .OnVerifyTextChanged(this, &SWidgetAnimationListItem::OnVerifyNameTextChanged) .OnTextCommitted(this, &SWidgetAnimationListItem::OnNameTextCommited) .IsSelected(this, &SWidgetAnimationListItem::IsSelectedExclusively) ], InOwnerTableView); } void BeginRename() { InlineTextBlock->EnterEditingMode(); } private: FText GetMovieSceneText() const { if (ListItem.IsValid()) { return ListItem.Pin()->Animation->GetDisplayName(); } return FText::GetEmpty(); } bool OnVerifyNameTextChanged(const FText& InText, FText& OutErrorMessage) { auto Animation = ListItem.Pin()->Animation; auto SequenceComp = Editor->GetSequenceComponent(); if (SequenceComp) { auto& SequenceArray = SequenceComp->GetSequenceArray(); auto ExistIndex = SequenceArray.IndexOfByPredicate([InText, this](const ULGUIPrefabSequence* Item) { if (ListItem.Pin()->Animation == Item) { return false; } return Item->GetDisplayName().EqualTo(InText); }); if (ExistIndex != INDEX_NONE) { OutErrorMessage = LOCTEXT("NameInUseByAnimation", "An animation with this name already exists"); return false; } } return true; } void OnNameTextCommited(const FText& InText, ETextCommit::Type CommitInfo) { auto Animation = ListItem.Pin()->Animation; // Name has already been checked in VerifyAnimationRename auto NewName = InText.ToString(); auto OldName = Animation->GetDisplayName().ToString(); //FObjectPropertyBase* ExistingProperty = CastField(Blueprint->ParentClass->FindPropertyByName(NewFName)); //const bool bBindWidgetAnim = ExistingProperty && FWidgetBlueprintEditorUtils::IsBindWidgetAnimProperty(ExistingProperty) && ExistingProperty->PropertyClass->IsChildOf(UWidgetAnimation::StaticClass()); const bool bValidName = !OldName.Equals(NewName) && !InText.IsEmpty(); const bool bCanRename = (bValidName/* || bBindWidgetAnim*/); const bool bNewAnimation = ListItem.Pin()->bNewAnimation; if (bCanRename) { FText TransactionName = bNewAnimation ? LOCTEXT("NewAnimation", "New Animation") : LOCTEXT("RenameAnimation", "Rename Animation"); { const FScopedTransaction Transaction(TransactionName); Animation->Modify(); Animation->SetDisplayNameString(NewName); if (bNewAnimation) { Editor->RefreshAnimationList(); ListItem.Pin()->bNewAnimation = false; } } } else if (bNewAnimation) { const FScopedTransaction Transaction(LOCTEXT("NewAnimation", "New Animation")); Editor->RefreshAnimationList(); ListItem.Pin()->bNewAnimation = false; } } private: TWeakPtr ListItem; SLGUIPrefabSequenceEditor* Editor = nullptr; TSharedPtr InlineTextBlock; }; SLGUIPrefabSequenceEditor::~SLGUIPrefabSequenceEditor() { FCoreUObjectDelegates::OnObjectsReplaced.Remove(OnObjectsReplacedHandle); LGUIEditorTools::OnEditingPrefabChanged.Remove(EditingPrefabChangedHandle); LGUIEditorTools::OnBeforeApplyPrefab.Remove(OnBeforeApplyPrefabHandle); } void SLGUIPrefabSequenceEditor::Construct(const FArguments& InArgs) { SAssignNew(AnimationListView, SWidgetAnimationListView) .SelectionMode(ESelectionMode::Single) .ListItemsSource(&Animations) .OnGenerateRow(this, &SLGUIPrefabSequenceEditor::OnGenerateRowForAnimationListView) .OnItemScrolledIntoView(this, &SLGUIPrefabSequenceEditor::OnItemScrolledIntoView) .OnSelectionChanged(this, &SLGUIPrefabSequenceEditor::OnAnimationListViewSelectionChanged) .OnContextMenuOpening(this, &SLGUIPrefabSequenceEditor::OnContextMenuOpening) ; ChildSlot [ SNew(SSplitter) +SSplitter::Slot() .Value(0.2f) [ SNew(SBox) .IsEnabled_Lambda([=, this]() { return WeakSequenceComponent.IsValid(); }) [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ SNew(SVerticalBox) + SVerticalBox::Slot() .Padding( 2 ) .AutoHeight() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() [ SNew(SButton) .Text_Lambda([=, this](){ if (WeakSequenceComponent.IsValid()) { auto Actor = WeakSequenceComponent->GetOwner(); if (Actor) { auto DisplayText = Actor->GetActorLabel() + TEXT(".") + WeakSequenceComponent->GetName(); return FText::FromString(DisplayText); } } return LOCTEXT("NullSequenceComponent", "Null (LGUIPrefabSequence)"); }) .ToolTipText(LOCTEXT("ObjectButtonTooltipText", "Actor.Component, click to select target")) .IsEnabled_Lambda([=, this](){ return WeakSequenceComponent.IsValid(); }) .ButtonStyle( FAppStyle::Get(), "PropertyEditor.AssetComboStyle" ) .ForegroundColor(FAppStyle::GetColor("PropertyEditor.AssetName.ColorAndOpacity")) .OnClicked_Lambda([=, this](){ if (WeakSequenceComponent.IsValid()) { GEditor->SelectNone(true, true); GEditor->SelectActor(WeakSequenceComponent->GetOwner(), true, true); GEditor->SelectComponent(WeakSequenceComponent.Get(), true, true); } return FReply::Handled(); }) ] +SHorizontalBox::Slot() .HAlign(HAlign_Right) .VAlign(VAlign_Center) [ PropertyCustomizationHelpers::MakeResetButton( FSimpleDelegate::CreateLambda([=, this]() { AssignLGUIPrefabSequenceComponent(nullptr); }) , LOCTEXT("ClearSequenceComponent", "Click to clear current selected LGUISequenceComponent, so we will not edit it here.") ) ] ] + SVerticalBox::Slot() .Padding( 2 ) .AutoHeight() [ SNew( SHorizontalBox ) + SHorizontalBox::Slot() .Padding(0) .VAlign( VAlign_Center ) .AutoWidth() [ SNew(SButton) .OnClicked(this, &SLGUIPrefabSequenceEditor::OnNewAnimationClicked) .Text(LOCTEXT("NewAnimationButtonText", "+ Animation")) ] + SHorizontalBox::Slot() .Padding(2.0f, 0.0f) .VAlign( VAlign_Center ) [ SAssignNew(SearchBoxPtr, SSearchBox) .HintText(LOCTEXT("Search Animations", "Search Animations")) .OnTextChanged(this, &SLGUIPrefabSequenceEditor::OnAnimationListViewSearchChanged) ] ] + SVerticalBox::Slot() .FillHeight(1.0f) [ SNew(SScrollBorder, AnimationListView.ToSharedRef()) [ AnimationListView.ToSharedRef() ] ] ] ] ] +SSplitter::Slot() .Value(0.8f) [ SAssignNew(PrefabSequenceEditor, SLGUIPrefabSequenceEditorWidget, nullptr) ] ]; CreateCommandList(); OnObjectsReplacedHandle = FCoreUObjectDelegates::OnObjectsReplaced.AddSP(this, &SLGUIPrefabSequenceEditor::OnObjectsReplaced); PrefabSequenceEditor->AssignSequence(GetLGUIPrefabSequence()); EditingPrefabChangedHandle = LGUIEditorTools::OnEditingPrefabChanged.AddRaw(this, &SLGUIPrefabSequenceEditor::OnEditingPrefabChanged); OnBeforeApplyPrefabHandle = LGUIEditorTools::OnBeforeApplyPrefab.AddRaw(this, &SLGUIPrefabSequenceEditor::OnBeforeApplyPrefab); } void SLGUIPrefabSequenceEditor::AssignLGUIPrefabSequenceComponent(TWeakObjectPtr InSequenceComponent) { WeakSequenceComponent = InSequenceComponent; RefreshAnimationList(); } ULGUIPrefabSequence* SLGUIPrefabSequenceEditor::GetLGUIPrefabSequence() const { if (CurrentSelectedAnimationIndex != INDEX_NONE) { ULGUIPrefabSequenceComponent* SequenceComponent = WeakSequenceComponent.Get(); return SequenceComponent ? SequenceComponent->GetSequenceByIndex(CurrentSelectedAnimationIndex) : nullptr; } return nullptr; } void SLGUIPrefabSequenceEditor::OnObjectsReplaced(const TMap& ReplacementMap) { ULGUIPrefabSequenceComponent* Component = WeakSequenceComponent.Get(true); ULGUIPrefabSequenceComponent* NewSequenceComponent = Component ? Cast(ReplacementMap.FindRef(Component)) : nullptr; if (NewSequenceComponent) { WeakSequenceComponent = NewSequenceComponent; PrefabSequenceEditor->AssignSequence(GetLGUIPrefabSequence()); } } TSharedRef SLGUIPrefabSequenceEditor::OnGenerateRowForAnimationListView(TSharedPtr InListItem, const TSharedRef& InOwnerTableView) { return SNew(SWidgetAnimationListItem, InOwnerTableView, this, InListItem); } void SLGUIPrefabSequenceEditor::OnAnimationListViewSelectionChanged(TSharedPtr InListItem, ESelectInfo::Type InSelectInfo) { CurrentSelectedAnimationIndex = INDEX_NONE; if (InListItem.IsValid()) { auto& SequenceArray = WeakSequenceComponent->GetSequenceArray(); for (int i = 0; i < SequenceArray.Num(); i++) { if (SequenceArray[i] == InListItem->Animation) { CurrentSelectedAnimationIndex = i; break; } } } PrefabSequenceEditor->AssignSequence(GetLGUIPrefabSequence()); } void SLGUIPrefabSequenceEditor::RefreshAnimationList() { if (WeakSequenceComponent.IsValid()) { Animations.Reset(); auto& SequenceArray = WeakSequenceComponent->GetSequenceArray(); for (auto& Item : SequenceArray) { Animations.Add(MakeShareable(new FWidgetAnimationListItem(Item))); } AnimationListView->RequestListRefresh(); if (Animations.Num() > 0) { AnimationListView->SetSelection(Animations[0]); } } } void SLGUIPrefabSequenceEditor::OnBeforeApplyPrefab(ULGUIPrefabHelperObject* InObject) { if (WeakSequenceComponent.IsValid()) { if (auto Actor = WeakSequenceComponent->GetOwner()) { if (InObject->IsActorBelongsToThis(Actor)) { this->AnimationListView->ClearSelection(); } } } } // Trigger when opening a new prefab void SLGUIPrefabSequenceEditor::OnEditingPrefabChanged(AActor* RootActor) { if (RootActor) { TArray ChildrenActors; RootActor->GetAttachedActors(ChildrenActors, true, true); for (AActor* ChildActor : ChildrenActors) { ULGUIPrefabHelperObject* PrefabHelperObject = LGUIEditorTools::GetPrefabHelperObject_WhichManageThisActor(RootActor); if (PrefabHelperObject) { //skip sub prefab's PrefabSequenceComponent if (PrefabHelperObject->IsActorBelongsToSubPrefab(ChildActor)) { continue; } } ULGUIPrefabSequenceComponent* PrefabSequencerComponent = ChildActor->FindComponentByClass(); if (PrefabSequencerComponent) { AssignLGUIPrefabSequenceComponent(PrefabSequencerComponent); } } } } void SLGUIPrefabSequenceEditor::OnAnimationListViewSearchChanged(const FText& InSearchText) { if (WeakSequenceComponent.IsValid()) { auto& SequenceArray = WeakSequenceComponent->GetSequenceArray(); if (!InSearchText.IsEmpty()) { struct Local { static void UpdateFilterStrings(ULGUIPrefabSequence* InAnimation, OUT TArray< FString >& OutFilterStrings) { OutFilterStrings.Add(InAnimation->GetName()); } }; TTextFilter TextFilter(TTextFilter::FItemToStringArray::CreateStatic(&Local::UpdateFilterStrings)); TextFilter.SetRawFilterText(InSearchText); SearchBoxPtr->SetError(TextFilter.GetFilterErrorText()); Animations.Reset(); for (ULGUIPrefabSequence* Animation : SequenceArray) { if (TextFilter.PassesFilter(Animation)) { Animations.Add(MakeShareable(new FWidgetAnimationListItem(Animation))); } } AnimationListView->RequestListRefresh(); } else { SearchBoxPtr->SetError(FText::GetEmpty()); RefreshAnimationList(); } } } void SLGUIPrefabSequenceEditor::OnItemScrolledIntoView(TSharedPtr InListItem, const TSharedPtr& InWidget) const { if (InListItem->bRenameRequestPending) { StaticCastSharedPtr(InWidget)->BeginRename(); InListItem->bRenameRequestPending = false; } } TSharedPtr SLGUIPrefabSequenceEditor::OnContextMenuOpening()const { FMenuBuilder MenuBuilder(true, CommandList.ToSharedRef()); MenuBuilder.BeginSection("Edit", LOCTEXT("Edit", "Edit")); { MenuBuilder.AddMenuEntry(FGenericCommands::Get().Rename); MenuBuilder.AddMenuEntry(FGenericCommands::Get().Duplicate); MenuBuilder.AddMenuSeparator(); MenuBuilder.AddMenuEntry(FGenericCommands::Get().Delete); //create fix button { auto SelectedItems = AnimationListView->GetSelectedItems(); if (SelectedItems.Num() == 1) { auto SelectedItem = SelectedItems[0]; if (!SelectedItem->Animation->IsObjectReferencesGood(WeakSequenceComponent->GetOwner())) { MenuBuilder.AddMenuSeparator(); MenuBuilder.AddMenuEntry( LOCTEXT("TryFixObjectReference", "Try fix object reference"), LOCTEXT("TryFixObjectReference_Tooltip", "LGUI can search target object by actor's path relative to ContextActor (Owner actor of LGUIPrefabSequenceComponent), so if ActorLabel and Actor's hierarchy is same as before, it is possible to fix the bad tracks."), FSlateIcon(), FUIAction(FExecuteAction::CreateLambda([=, this]() { SelectedItem->Animation->FixObjectReferences(WeakSequenceComponent->GetOwner()); })) ); } } } } MenuBuilder.EndSection(); return MenuBuilder.MakeWidget(); } void SLGUIPrefabSequenceEditor::CreateCommandList() { CommandList = MakeShareable(new FUICommandList); CommandList->MapAction( FGenericCommands::Get().Duplicate, FExecuteAction::CreateSP(this, &SLGUIPrefabSequenceEditor::OnDuplicateAnimation) ); CommandList->MapAction( FGenericCommands::Get().Delete, FExecuteAction::CreateSP(this, &SLGUIPrefabSequenceEditor::OnDeleteAnimation) ); CommandList->MapAction( FGenericCommands::Get().Rename, FExecuteAction::CreateSP(this, &SLGUIPrefabSequenceEditor::OnRenameAnimation) ); } FReply SLGUIPrefabSequenceEditor::OnNewAnimationClicked() { if (WeakSequenceComponent.IsValid()) { auto Sequence = WeakSequenceComponent->AddNewAnimation(); bool bRequestRename = true; bool bNewAnimation = true; int32 NewIndex = Animations.Add(MakeShareable(new FWidgetAnimationListItem(Sequence, bRequestRename, bNewAnimation))); AnimationListView->RequestScrollIntoView(Animations[NewIndex]); } return FReply::Handled(); } void SLGUIPrefabSequenceEditor::OnDuplicateAnimation() { if (WeakSequenceComponent.IsValid()) { GEditor->BeginTransaction(LOCTEXT("DuplicateAnimation_Transaction", "LGUISequence Duplicate Animation")); WeakSequenceComponent->Modify(); auto Sequence = WeakSequenceComponent->DuplicateAnimationByIndex(CurrentSelectedAnimationIndex); GEditor->EndTransaction(); if (Sequence) { bool bRequestRename = true; bool bNewAnimation = true; int32 NewIndex = Animations.Insert(MakeShareable(new FWidgetAnimationListItem(Sequence, bRequestRename, bNewAnimation)), CurrentSelectedAnimationIndex + 1); AnimationListView->RequestScrollIntoView(Animations[NewIndex]); } } } void SLGUIPrefabSequenceEditor::OnDeleteAnimation() { if (WeakSequenceComponent.IsValid()) { GEditor->BeginTransaction(LOCTEXT("DeleteAnimation_Transaction", "LGUISequence Delete Animation")); WeakSequenceComponent->Modify(); bool bDeleted = WeakSequenceComponent->DeleteAnimationByIndex(CurrentSelectedAnimationIndex); GEditor->EndTransaction(); if (bDeleted) { Animations.RemoveAt(CurrentSelectedAnimationIndex); AnimationListView->RebuildList(); CurrentSelectedAnimationIndex = INDEX_NONE; PrefabSequenceEditor->AssignSequence(nullptr); } } } void SLGUIPrefabSequenceEditor::OnRenameAnimation() { TArray< TSharedPtr > SelectedAnimations = AnimationListView->GetSelectedItems(); check(SelectedAnimations.Num() == 1); TSharedPtr SelectedAnimation = SelectedAnimations[0]; SelectedAnimation->bRenameRequestPending = true; AnimationListView->RequestScrollIntoView(SelectedAnimation); } #undef LOCTEXT_NAMESPACE