// Copyright Epic Games, Inc. All Rights Reserved. #include "SequencerCommonHelpers.h" #include "FrameNumberDetailsCustomization.h" #include "IDetailsView.h" #include "ISequencerSection.h" #include "IStructureDetailsView.h" #include "MVVM/Extensions/ITrackAreaExtension.h" #include "MVVM/ViewModels/ChannelModel.h" #include "MVVM/ViewModels/ViewModelIterators.h" #include "MVVM/ViewModels/ObjectBindingModel.h" #include "MVVM/ViewModels/SectionModel.h" #include "MVVM/ViewModels/TrackModel.h" #include "MVVM/ViewModels/ViewModel.h" #include "MVVM/ViewModels/OutlinerViewModel.h" #include "MVVM/ViewModels/SequencerEditorViewModel.h" #include "MVVM/ViewModels/VirtualTrackArea.h" #include "MVVM/Views/ITrackAreaHotspot.h" #include "Modules/ModuleManager.h" #include "MovieScene.h" #include "MovieSceneSectionDetailsCustomization.h" #include "MovieSceneSequence.h" #include "PropertyEditorModule.h" #include "SSequencer.h" #include "Sequencer.h" #include "SequencerContextMenus.h" #include "SequencerSelectedKey.h" #include "Styling/CoreStyle.h" void SequencerHelpers::GetAllChannels(TSharedPtr DataModel, TSet>& Channels) { using namespace UE::Sequencer; if (DataModel) { constexpr bool bIncludeThis = true; for (const FViewModelPtr& Child : DataModel->GetDescendants(bIncludeThis)) { if (TSharedPtr TrackArea = Child.ImplicitCast()) { for (const FViewModelPtr& TrackAreaModel : TrackArea->GetTrackAreaModelList()) { if (TViewModelPtr Channel = TrackAreaModel.ImplicitCast()) { Channels.Add(Channel); } } } else if (TSharedPtr Channel = Child.ImplicitCast()) { Channels.Add(Channel); } } } } void SequencerHelpers::GetAllKeyAreas(TSharedPtr DataModel, TSet>& Channels) { using namespace UE::Sequencer; if (DataModel) { constexpr bool bIncludeThis = true; for (const FViewModelPtr& Child : DataModel->GetDescendants(bIncludeThis)) { if (TSharedPtr TrackArea = Child.ImplicitCast()) { for (const FViewModelPtr& TrackAreaModel : TrackArea->GetTrackAreaModelList()) { if (TViewModelPtr Channel = TrackAreaModel.ImplicitCast()) { Channels.Add(Channel->GetKeyArea()); } } } else if (TSharedPtr Channel = Child.ImplicitCast()) { Channels.Add(Channel->GetKeyArea()); } } } } void SequencerHelpers::GetAllSections(TSharedPtr DataModel, TSet>& Sections) { using namespace UE::Sequencer; if (DataModel) { constexpr bool bIncludeThis = true; for (TSharedPtr Section : TParentFirstChildIterator(DataModel, bIncludeThis)) { Sections.Add(Section->GetSection()); } } } int32 SequencerHelpers::GetSectionFromTime(TArrayView InSections, FFrameNumber Time) { FFrameNumber ClosestLowerBound = TNumericLimits::Max(); TOptional MaxOverlapPriority, MaxProximalPriority; int32 MostRelevantIndex = INDEX_NONE; for (int32 Index = 0; Index < InSections.Num(); ++Index) { const UMovieSceneSection* Section = InSections[Index]; if (Section) { const int32 ThisSectionPriority = Section->GetOverlapPriority(); TRange SectionRange = Section->GetRange(); // If the specified time is within the section bounds if (SectionRange.Contains(Time)) { if (ThisSectionPriority >= MaxOverlapPriority.Get(ThisSectionPriority)) { MaxOverlapPriority = ThisSectionPriority; MostRelevantIndex = Index; } } // Check for nearby sections if there is nothing overlapping else if (!MaxOverlapPriority.IsSet() && SectionRange.HasLowerBound()) { const FFrameNumber LowerBoundValue = SectionRange.GetLowerBoundValue(); // If this section exists beyond the current time, we can choose it if its closest to the time if (LowerBoundValue >= Time) { if ( (LowerBoundValue < ClosestLowerBound) || (LowerBoundValue == ClosestLowerBound && ThisSectionPriority >= MaxProximalPriority.Get(ThisSectionPriority)) ) { MostRelevantIndex = Index; ClosestLowerBound = LowerBoundValue; MaxProximalPriority = ThisSectionPriority; } } } } } // If we didn't find one, use the last one (or return -1) if (MostRelevantIndex == -1) { MostRelevantIndex = InSections.Num() - 1; } return MostRelevantIndex; } void SequencerHelpers::GetDescendantNodes(TSharedRef DataModel, TSet>& Nodes) { using namespace UE::Sequencer; for (TSharedPtr ChildNode : DataModel->GetChildren()) { if (ChildNode->IsA()) { Nodes.Add(ChildNode.ToSharedRef()); } GetDescendantNodes(ChildNode.ToSharedRef(), Nodes); } } bool IsSectionSelectedInNode(FSequencer& Sequencer, TSharedPtr InModel) { using namespace UE::Sequencer; if (ITrackAreaExtension* TrackArea = InModel->CastThis()) { for (TSharedPtr TrackAreaModel : TrackArea->GetTrackAreaModelList()) { constexpr bool bIncludeThis = true; for (TSharedPtr Section : TParentFirstChildIterator(TrackAreaModel, bIncludeThis)) { if (Sequencer.GetSelection().IsSelected(Section)) { return true; } } } } return false; } bool AreKeysSelectedInNode(FSequencer& Sequencer, TSharedPtr InModel) { TSet> Channels; SequencerHelpers::GetAllChannels(InModel, Channels); for (const FSequencerSelectedKey& Key : Sequencer.GetSelection().GetSelectedKeys()) { if (Channels.Contains(Key.WeakChannel.Pin())) { return true; } } return false; } void SequencerHelpers::PerformDefaultSelection(FSequencer& Sequencer, const FPointerEvent& MouseEvent) { using namespace UE::Sequencer; // @todo: selection in transactions FHotspotSelectionManager SelectionManager(&MouseEvent, &Sequencer); TSharedPtr Hotspot = Sequencer.GetViewModel()->GetTrackArea()->GetHotspot(); if (TSharedPtr MouseHandler = HotspotCast(Hotspot)) { MouseHandler->HandleMouseSelection(SelectionManager); } else { // No hotspot so clear the selection if we're not adding to it SelectionManager.ConditionallyClearSelection(); } } TSharedPtr SequencerHelpers::SummonContextMenu(FSequencer& Sequencer, const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { using namespace UE::Sequencer; // @todo sequencer replace with UI Commands instead of faking it // Attempt to paste into either the current node selection, or the clicked on track TSharedRef SequencerWidget = StaticCastSharedRef(Sequencer.GetSequencerWidget()); const FFrameNumber PasteAtTime = Sequencer.GetLocalTime().Time.FrameNumber; // The menu are generated through reflection and sometime the API exposes some recursivity (think about a Widget returning it parent which is also a Widget). Just by reflection // it is not possible to determine when the root object is reached. It needs a kind of simulation which is not implemented. Also, even if the recursivity was correctly handled, the possible // permutations tend to grow exponentially. Until a clever solution is found, the simple approach is to disable recursively searching those menus. User can still search the current one though. // See UE-131257 const bool bInRecursivelySearchable = false; const bool bShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, Sequencer.GetCommandBindings(), nullptr, false, &FCoreStyle::Get(), true, NAME_None, bInRecursivelySearchable); TSharedPtr Hotspot = Sequencer.GetViewModel()->GetTrackArea()->GetHotspot(); if (Hotspot.IsValid() && Hotspot->PopulateContextMenu(MenuBuilder, PasteAtTime)) { return MenuBuilder.MakeWidget(); } else if (Sequencer.GetClipboardStack().Num() != 0) { TSharedPtr PasteMenu = FPasteContextMenu::CreateMenu(Sequencer, SequencerWidget->GeneratePasteArgs(PasteAtTime)); if (PasteMenu.IsValid() && PasteMenu->IsValidPaste()) { PasteMenu->PopulateMenu(MenuBuilder); return MenuBuilder.MakeWidget(); } } return nullptr; } /** A widget which wraps the section details view which is an FNotifyHook which is used to forward changes to the section to sequencer. */ class SSectionDetailsNotifyHookWrapper : public SCompoundWidget, public FNotifyHook { public: SLATE_BEGIN_ARGS(SSectionDetailsNotifyHookWrapper) {} SLATE_END_ARGS(); void Construct(FArguments InArgs) { } void SetDetailsAndSequencer(TSharedRef InDetailsPanel, TSharedRef InSequencer) { ChildSlot [ InDetailsPanel ]; Sequencer = InSequencer; } //~ FNotifyHook interface virtual void NotifyPostChange(const FPropertyChangedEvent& PropertyChangedEvent, FProperty* PropertyThatChanged) override { Sequencer->NotifyMovieSceneDataChanged(EMovieSceneDataChangeType::TrackValueChanged); } private: TSharedPtr Sequencer; }; void SequencerHelpers::AddPropertiesMenu(FSequencer& Sequencer, FMenuBuilder& MenuBuilder, const TArray>& Sections) { using namespace UE::Sequencer; TSharedRef DetailsNotifyWrapper = SNew(SSectionDetailsNotifyHookWrapper); FDetailsViewArgs DetailsViewArgs; { DetailsViewArgs.bAllowSearch = false; DetailsViewArgs.bCustomFilterAreaLocation = true; DetailsViewArgs.bCustomNameAreaLocation = true; DetailsViewArgs.bHideSelectionTip = true; DetailsViewArgs.bLockable = false; DetailsViewArgs.bSearchInitialKeyFocus = true; DetailsViewArgs.bUpdatesFromSelection = false; DetailsViewArgs.bShowOptions = false; DetailsViewArgs.bShowModifiedPropertiesOption = false; DetailsViewArgs.NotifyHook = &DetailsNotifyWrapper.Get(); DetailsViewArgs.ColumnWidth = 0.45f; } // We pass the current scene to the UMovieSceneSection customization so we can get the overall bounds of the section when we change a section from infinite->bounded. UMovieScene* CurrentScene = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene(); TSharedRef> NumericTypeInterface = Sequencer.GetNumericTypeInterface(); TSharedRef DetailsView = FModuleManager::GetModuleChecked("PropertyEditor").CreateDetailView(DetailsViewArgs); DetailsView->RegisterInstancedCustomPropertyTypeLayout("FrameNumber", FOnGetPropertyTypeCustomizationInstance::CreateLambda([=]() { return MakeShared(NumericTypeInterface); })); DetailsView->RegisterInstancedCustomPropertyLayout(UMovieSceneSection::StaticClass(), FOnGetDetailCustomizationInstance::CreateLambda([=]() { return MakeShared(NumericTypeInterface, CurrentScene); })); // Let section interfaces further customize the properties details view. TSharedRef SequencerNodeTree = Sequencer.GetNodeTree(); for (TWeakObjectPtr Section : Sections) { if (Section.IsValid()) { TSharedPtr SectionHandle = SequencerNodeTree->GetSectionModel(Cast(Section)); if (SectionHandle) { TSharedPtr SectionInterface = SectionHandle->GetSectionInterface(); FSequencerSectionPropertyDetailsViewCustomizationParams CustomizationDetails( SectionInterface.ToSharedRef(), Sequencer.AsShared(), *SectionHandle->GetParentTrackExtension()->GetTrackEditor().Get()); TSharedPtr ParentObjectBindingNode = SectionHandle->FindAncestorOfType(); if (ParentObjectBindingNode.IsValid()) { CustomizationDetails.ParentObjectBindingGuid = ParentObjectBindingNode->GetObjectGuid(); } SectionInterface->CustomizePropertiesDetailsView(DetailsView, CustomizationDetails); } } } Sequencer.OnInitializeDetailsPanel().Broadcast(DetailsView, Sequencer.AsShared()); DetailsView->SetObjects(Sections); DetailsNotifyWrapper->SetDetailsAndSequencer(DetailsView, Sequencer.AsShared()); MenuBuilder.AddWidget(DetailsNotifyWrapper, FText::GetEmpty(), true); }