// Copyright Epic Games, Inc. All Rights Reserved. #include "SOutlinerTreeView.h" #include "Framework/Application/SlateApplication.h" #include "Styling/AppStyle.h" #include "ISceneOutlinerColumn.h" #include "ISceneOutlinerMode.h" #include "SceneOutlinerPublicTypes.h" #include "DragAndDrop/DecoratedDragDropOp.h" #include "SceneOutlinerDragDrop.h" #include "SSceneOutliner.h" #include "Styling/StyleColors.h" #include "FolderTreeItem.h" #define LOCTEXT_NAMESPACE "SSceneOutliner" static void UpdateOperationDecorator(const FDragDropEvent& Event, const FSceneOutlinerDragValidationInfo& ValidationInfo) { const FSlateBrush* Icon = ValidationInfo.IsValid() ? FAppStyle::GetBrush(TEXT("Graph.ConnectorFeedback.OK")) : FAppStyle::GetBrush(TEXT("Graph.ConnectorFeedback.Error")); FDragDropOperation* Operation = Event.GetOperation().Get(); if (Operation && Operation->IsOfType()) { auto* DecoratedOp = static_cast(Operation); DecoratedOp->SetToolTip(ValidationInfo.ValidationText, Icon); } } static void ResetOperationDecorator(const FDragDropEvent& Event) { FDragDropOperation* Operation = Event.GetOperation().Get(); if (Operation) { if (Operation->IsOfType()) { static_cast(Operation)->ResetTooltip(); } else if (Operation->IsOfType()) { static_cast(Operation)->ResetToDefaultToolTip(); } } } static FReply HandleOnDragDetected( const FGeometry& MyGeometry, const FPointerEvent& MouseEvent, TWeakPtr Table ) { auto TablePtr = Table.Pin(); if (TablePtr.IsValid() && MouseEvent.IsMouseButtonDown( EKeys::LeftMouseButton )) { auto Operation = TablePtr->GetOutlinerPtr().Pin()->CreateDragDropOperation(MouseEvent, TablePtr->GetSelectedItems()); if (Operation.IsValid()) { return FReply::Handled().BeginDragDrop(Operation.ToSharedRef()); } } return FReply::Unhandled(); } FReply HandleDrop(TSharedPtr SceneOutlinerPtr, const FDragDropEvent& DragDropEvent, ISceneOutlinerTreeItem& DropTarget, FSceneOutlinerDragValidationInfo& ValidationInfo, bool bApplyDrop = false) { if (!SceneOutlinerPtr.IsValid()) { return FReply::Unhandled(); } // Don't handle this if we're not showing a hierarchy const FSharedSceneOutlinerData& SharedData = SceneOutlinerPtr->GetSharedData(); if (!SharedData.bShowParentTree) { return FReply::Unhandled(); } // Don't handle this if the scene outliner is not in a mode which supports drag and drop if (!SceneOutlinerPtr->CanSupportDragAndDrop()) { return FReply::Unhandled(); } FSceneOutlinerDragDropPayload DraggedObjects(*DragDropEvent.GetOperation()); // Validate now to make sure we don't doing anything we shouldn't if (!SceneOutlinerPtr->ParseDragDrop(DraggedObjects, *DragDropEvent.GetOperation())) { return FReply::Unhandled(); } ValidationInfo = SceneOutlinerPtr->ValidateDrop(StaticCast(DropTarget), DraggedObjects); if (!ValidationInfo.IsValid()) { // Return handled here to stop anything else trying to handle it - the operation is invalid as far as we're concerned return FReply::Handled(); } if (bApplyDrop) { SceneOutlinerPtr->OnDropPayload(DropTarget, DraggedObjects, ValidationInfo); } return FReply::Handled(); } FReply HandleDropFromWeak(TWeakPtr SceneOutlinerWeak, const FDragDropEvent& DragDropEvent, FSceneOutlinerDragValidationInfo& ValidationInfo, bool bApplyDrop = false) { const ISceneOutlinerMode* Mode = SceneOutlinerWeak.IsValid() ? SceneOutlinerWeak.Pin()->GetMode() : nullptr; FFolder::FRootObject RootObject = Mode ? Mode->GetRootObject() : FFolder::GetInvalidRootObject(); FFolder RootFolder(RootObject); FFolderTreeItem DropTarget(RootFolder); return HandleDrop(SceneOutlinerWeak.Pin(), DragDropEvent, DropTarget, ValidationInfo, bApplyDrop); } void SSceneOutlinerTreeView::Construct(const SSceneOutlinerTreeView::FArguments& InArgs, TSharedRef Owner) { SceneOutlinerWeak = Owner; STreeView::Construct(InArgs); } void SSceneOutlinerTreeView::FlashHighlightOnItem( FSceneOutlinerTreeItemPtr FlashHighlightOnItem ) { TSharedPtr< SSceneOutlinerTreeRow > RowWidget = StaticCastSharedPtr< SSceneOutlinerTreeRow >( WidgetGenerator.GetWidgetForItem( FlashHighlightOnItem ) ); if( RowWidget.IsValid() ) { RowWidget->FlashHighlight(); } } FReply SSceneOutlinerTreeView::OnDragOver(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) { FSceneOutlinerDragValidationInfo ValidationInfo = FSceneOutlinerDragValidationInfo::Invalid(); auto Reply = HandleDropFromWeak(SceneOutlinerWeak, DragDropEvent, ValidationInfo); if (Reply.IsEventHandled()) { UpdateOperationDecorator(DragDropEvent, ValidationInfo); } return Reply; } void SSceneOutlinerTreeView::OnDragLeave(const FDragDropEvent& DragDropEvent) { if (!SceneOutlinerWeak.IsValid()) { return; } if( SceneOutlinerWeak.Pin()->GetSharedData().bShowParentTree ) { ResetOperationDecorator(DragDropEvent); } } FReply SSceneOutlinerTreeView::OnDrop(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) { FSceneOutlinerDragValidationInfo ValidationInfo = FSceneOutlinerDragValidationInfo::Invalid(); return HandleDropFromWeak(SceneOutlinerWeak, DragDropEvent, ValidationInfo, true); } void SSceneOutlinerTreeView::Private_UpdateParentHighlights() { this->ClearHighlightedItems(); // For the Outliner, we want to highlight parent items even if the current selection is not visible (i.e collapsed) for( typename TItemSet::TConstIterator SelectedItemIt(SelectedItems); SelectedItemIt; ++SelectedItemIt ) { auto Parent = (*SelectedItemIt)->GetParent(); while (Parent.IsValid()) { Private_SetItemHighlighted(Parent, true); Parent = Parent->GetParent(); } } } FReply SSceneOutlinerTreeRow::OnDrop( const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent ) { auto ItemPtr = Item.Pin(); auto SceneOutlinerPtr = SceneOutlinerWeak.Pin(); if (ItemPtr.IsValid() && SceneOutlinerPtr.IsValid()) { FSceneOutlinerDragValidationInfo ValidationInfo = FSceneOutlinerDragValidationInfo::Invalid(); return HandleDrop(SceneOutlinerPtr, DragDropEvent, *ItemPtr, ValidationInfo, true); } return FReply::Unhandled(); } void SSceneOutlinerTreeRow::OnDragEnter( const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent ) { auto ItemPtr = Item.Pin(); auto SceneOutlinerPtr = SceneOutlinerWeak.Pin(); if (ItemPtr.IsValid() && SceneOutlinerPtr.IsValid()) { FSceneOutlinerDragValidationInfo ValidationInfo = FSceneOutlinerDragValidationInfo::Invalid(); FReply Reply = HandleDrop(SceneOutlinerPtr, DragDropEvent, *ItemPtr, ValidationInfo, false); if (Reply.IsEventHandled()) { UpdateOperationDecorator(DragDropEvent, ValidationInfo); } } } void SSceneOutlinerTreeRow::OnDragLeave( const FDragDropEvent& DragDropEvent ) { auto ItemPtr = Item.Pin(); auto SceneOutlinerPtr = SceneOutlinerWeak.Pin(); ResetOperationDecorator(DragDropEvent); } FReply SSceneOutlinerTreeRow::OnDragOver(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) { auto SceneOutlinerPtr = SceneOutlinerWeak.Pin(); if (SSceneOutliner* SceneOutliner = SceneOutlinerPtr.Get()) { if (const auto* ItemPtr = Item.Pin().Get()) { return SceneOutliner->OnDragOverItem(DragDropEvent, *ItemPtr); } return FReply::Unhandled(); } return FReply::Handled(); } FReply SSceneOutlinerTreeRow::OnMouseButtonDown( const FGeometry& MyGeometry, const FPointerEvent& MouseEvent ) { auto ItemPtr = Item.Pin(); if (ItemPtr.IsValid() && ItemPtr->CanInteract()) { if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) { FReply Reply = SMultiColumnTableRow::OnMouseButtonDown( MyGeometry, MouseEvent ); if (SceneOutlinerWeak.Pin()->CanSupportDragAndDrop()) { return Reply.DetectDrag( SharedThis(this) , EKeys::LeftMouseButton ); } return Reply.PreventThrottling(); } } return FReply::Handled(); } FReply SSceneOutlinerTreeRow::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { auto ItemPtr = Item.Pin(); // We don't to change the selection when it is a left click since this was handle in the on mouse down if (ItemPtr.IsValid() && ItemPtr->CanInteract()) { return SMultiColumnTableRow::OnMouseButtonUp(MyGeometry, MouseEvent); } return FReply::Handled(); } FReply SSceneOutlinerTreeRow::OnMouseButtonDoubleClick(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { auto ItemPtr = Item.Pin(); // We don't want to act on double click on an item that can't be interacted with if (ItemPtr.IsValid() && ItemPtr->CanInteract()) { return SMultiColumnTableRow::OnMouseButtonDoubleClick(MyGeometry, MouseEvent); } return FReply::Handled(); } TSharedRef SSceneOutlinerTreeRow::GenerateWidgetForColumn( const FName& ColumnName ) { auto ItemPtr = Item.Pin(); if (!ItemPtr.IsValid()) { return SNullWidget::NullWidget; } auto Outliner = SceneOutlinerWeak.Pin(); check(Outliner.IsValid()); // Create the widget for this item TSharedRef NewItemWidget = SNullWidget::NullWidget; auto Column = Outliner->GetColumns().FindRef(ColumnName); if (Column.IsValid()) { NewItemWidget = Column->ConstructRowWidget(ItemPtr.ToSharedRef(), *this); } if( ColumnName == FSceneOutlinerBuiltInColumnTypes::Label() ) { // The first column gets the tree expansion arrow for this row return SNew(SBox) .MinDesiredHeight(static_cast(FSceneOutlinerDefaultTreeItemMetrics::RowHeight())) [ SNew( SHorizontalBox ) +SHorizontalBox::Slot() .AutoWidth() .Padding(6.f, 0.f, 0.f, 0.f) [ SNew( SExpanderArrow, SharedThis(this) ).IndentAmount(12) ] +SHorizontalBox::Slot() .FillWidth(1.0f) [ NewItemWidget ] ]; } else { // Other columns just get widget content -- no expansion arrow needed return NewItemWidget; } } void SSceneOutlinerTreeRow::Construct( const FArguments& InArgs, const TSharedRef& OutlinerTreeView, TSharedRef SceneOutliner ) { Item = InArgs._Item->AsShared(); SceneOutlinerWeak = SceneOutliner; LastHighlightInteractionTime = 0.0; auto Args = FSuperRowType::FArguments() .Style(&FAppStyle::Get().GetWidgetStyle("SceneOutliner.TableViewRow")); Args.OnDragDetected_Static(HandleOnDragDetected, TWeakPtr(OutlinerTreeView)); SMultiColumnTableRow::Construct(Args, OutlinerTreeView); } const float SSceneOutlinerTreeRow::HighlightRectLeftOffset = 0.0f; const float SSceneOutlinerTreeRow::HighlightRectRightOffset = 0.0f; const float SSceneOutlinerTreeRow::HighlightTargetSpringConstant = 25.0f; const float SSceneOutlinerTreeRow::HighlightTargetEffectDuration = 0.5f; const float SSceneOutlinerTreeRow::HighlightTargetOpacity = 0.8f; const float SSceneOutlinerTreeRow::LabelChangedAnimOffsetPercent = 0.2f; void SSceneOutlinerTreeRow::FlashHighlight() { LastHighlightInteractionTime = FSlateApplication::Get().GetCurrentTime(); } void SSceneOutlinerTreeRow::Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) { // Call parent implementation. SCompoundWidget::Tick( AllottedGeometry, InCurrentTime, InDeltaTime ); // We'll draw with the 'focused' look if we're either focused or we have a context menu summoned const bool bShouldAppearFocused = HasKeyboardFocus(); // Update highlight 'target' effect { const float HighlightLeftX = HighlightRectLeftOffset; const float HighlightRightX = HighlightRectRightOffset + AllottedGeometry.GetLocalSize().X; HighlightTargetLeftSpring.SetTarget( HighlightLeftX ); HighlightTargetRightSpring.SetTarget( HighlightRightX ); float TimeSinceHighlightInteraction = (float)( InCurrentTime - LastHighlightInteractionTime ); if( TimeSinceHighlightInteraction <= HighlightTargetEffectDuration || bShouldAppearFocused ) { HighlightTargetLeftSpring.Tick( InDeltaTime ); HighlightTargetRightSpring.Tick( InDeltaTime ); } } } int32 SSceneOutlinerTreeRow::OnPaint( const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled ) const { int32 StartLayer = SMultiColumnTableRow::OnPaint( Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled ); const int32 TextLayer = 1; // See if a disabled effect should be used bool bEnabled = ShouldBeEnabled( bParentEnabled ); ESlateDrawEffect DrawEffects = (bEnabled) ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect; const double CurrentTime = FSlateApplication::Get().GetCurrentTime(); // We'll draw with the 'focused' look if we're either focused or we have a context menu summoned const bool bShouldAppearFocused = HasKeyboardFocus(); // Draw highlight targeting effect const float TimeSinceHighlightInteraction = (float)( CurrentTime - LastHighlightInteractionTime ); if( TimeSinceHighlightInteraction <= HighlightTargetEffectDuration ) { // Compute animation progress float EffectAlpha = FMath::Clamp( TimeSinceHighlightInteraction / HighlightTargetEffectDuration, 0.0f, 1.0f ); EffectAlpha = 1.0f - EffectAlpha * EffectAlpha; // Inverse square falloff (looks nicer!) // Apply extra opacity falloff when dehighlighting float EffectOpacity = EffectAlpha; // Figure out a universally visible highlight color. FLinearColor HighlightTargetColorAndOpacity = ( (FLinearColor::White - GetColorAndOpacity())*0.5f + FLinearColor(+0.4f, +0.1f, -0.2f)) * InWidgetStyle.GetColorAndOpacityTint(); HighlightTargetColorAndOpacity.A = HighlightTargetOpacity * EffectOpacity * 255.0f; // Compute the bounds offset of the highlight target from where the highlight target spring // extents currently lie. This is used to "grow" or "shrink" the highlight as needed. const float LabelChangedAnimOffset = LabelChangedAnimOffsetPercent * AllottedGeometry.GetLocalSize().Y; // Choose an offset amount depending on whether we're highlighting, or clearing highlight const float EffectOffset = EffectAlpha * LabelChangedAnimOffset; const float HighlightLeftX = HighlightTargetLeftSpring.GetPosition() - EffectOffset; const float HighlightRightX = HighlightTargetRightSpring.GetPosition() + EffectOffset; const float HighlightTopY = 0.0f - LabelChangedAnimOffset; const float HighlightBottomY = AllottedGeometry.GetLocalSize().Y + EffectOffset; const FVector2D DrawPosition = FVector2D( HighlightLeftX, HighlightTopY ); const FVector2D DrawSize = FVector2D( HighlightRightX - HighlightLeftX, HighlightBottomY - HighlightTopY ); const FSlateBrush* StyleInfo = FAppStyle::GetBrush("SceneOutliner.ChangedItemHighlight"); // NOTE: We rely on scissor clipping for the highlight rectangle FSlateDrawElement::MakeBox( OutDrawElements, LayerId + TextLayer, AllottedGeometry.ToPaintGeometry( DrawSize, FSlateLayoutTransform(DrawPosition) ), // Position, Size, Scale StyleInfo, // Style DrawEffects, // Effects to use HighlightTargetColorAndOpacity ); // Color } return FMath::Max(StartLayer, LayerId + TextLayer); } void SSceneOutlinerPinnedTreeRow::Construct(const FArguments& InArgs, const TSharedRef& OutlinerTreeView, TSharedRef SceneOutliner) { Item = InArgs._Item->AsShared(); SceneOutlinerWeak = SceneOutliner; OutlinerTreeViewWeak = OutlinerTreeView; FSuperRowType::FArguments Args = FSuperRowType::FArguments() .Style(&FAppStyle::Get().GetWidgetStyle("SceneOutliner.TableViewRow")); Args.OnDragDetected_Static(HandleOnDragDetected, TWeakPtr(OutlinerTreeView)); SMultiColumnTableRow::Construct(Args, OutlinerTreeView); } TSharedRef SSceneOutlinerPinnedTreeRow::GenerateWidgetForColumn(const FName& ColumnName) { TSharedPtr ItemPtr = Item.Pin(); if (!ItemPtr.IsValid()) { return SNullWidget::NullWidget; } TSharedPtr Outliner = SceneOutlinerWeak.Pin(); check(Outliner.IsValid()); // Create the widget for this item TSharedRef NewItemWidget = SNullWidget::NullWidget; TSharedPtr Column = Outliner->GetColumns().FindRef(ColumnName); if (Column.IsValid()) { // Construct the actual column widget first TSharedRef ActualWidget = Column->ConstructRowWidget(ItemPtr.ToSharedRef(), *this); if (ColumnName == FSceneOutlinerBuiltInColumnTypes::Label()) { // We add space for the expander arrow for the label widget to make sure the spacing/indentation is consistent with non-pinned rows NewItemWidget = SNew(SBox) .MinDesiredHeight(static_cast(FSceneOutlinerDefaultTreeItemMetrics::RowHeight())) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .Padding(6.f, 0.f, 0.f, 0.f) [ SNew(SExpanderArrow, SharedThis(this)).IndentAmount(12) .Visibility(EVisibility::Hidden) // Hidden SExpanderArrow to occupy the same space as non-pinned rows ] + SHorizontalBox::Slot() .FillWidth(1.0f) [ ActualWidget ] ]; } else { if (ActualWidget != SNullWidget::NullWidget) { // Get the sorted column IDs from the outliner TArray SortedColumnIDs; Outliner->GetSortedColumnIDs(SortedColumnIDs); // Get the current column and label column index to compare int32 ColumnIndex = SortedColumnIDs.Find(ColumnName); int32 LabelColumnIndex = SortedColumnIDs.Find(FSceneOutlinerBuiltInColumnTypes::Label()); // If either of the columns don't exist, this widget does not need to occupy space if (ColumnIndex == INDEX_NONE || LabelColumnIndex == INDEX_NONE) { ActualWidget->SetVisibility(EVisibility::Collapsed); } // If this column is to the LEFT of the label column, it is hidden but occupies space to ensure proper indentation if (ColumnIndex < LabelColumnIndex) { ActualWidget->SetVisibility(EVisibility::Hidden); } // If this column is to the RIGHT of the label colum, it should not occupy space else { ActualWidget->SetVisibility(EVisibility::Collapsed); } } NewItemWidget = ActualWidget; } } return NewItemWidget; } FReply SSceneOutlinerPinnedTreeRow::OnDrop(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) { TSharedPtr ItemPtr = Item.Pin(); TSharedPtr SceneOutlinerPtr = SceneOutlinerWeak.Pin(); if (ItemPtr.IsValid() && SceneOutlinerPtr.IsValid()) { FSceneOutlinerDragValidationInfo ValidationInfo = FSceneOutlinerDragValidationInfo::Invalid(); return HandleDrop(SceneOutlinerPtr, DragDropEvent, *ItemPtr, ValidationInfo, true); } return FReply::Unhandled(); } void SSceneOutlinerPinnedTreeRow::OnDragEnter(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) { TSharedPtr ItemPtr = Item.Pin(); TSharedPtr SceneOutlinerPtr = SceneOutlinerWeak.Pin(); if (ItemPtr.IsValid() && SceneOutlinerPtr.IsValid()) { FSceneOutlinerDragValidationInfo ValidationInfo = FSceneOutlinerDragValidationInfo::Invalid(); FReply Reply = HandleDrop(SceneOutlinerPtr, DragDropEvent, *ItemPtr, ValidationInfo, false); if (Reply.IsEventHandled()) { UpdateOperationDecorator(DragDropEvent, ValidationInfo); } } } void SSceneOutlinerPinnedTreeRow::OnDragLeave(const FDragDropEvent& DragDropEvent) { ResetOperationDecorator(DragDropEvent); } FReply SSceneOutlinerPinnedTreeRow::OnDragOver(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) { TSharedPtr SceneOutlinerPtr = SceneOutlinerWeak.Pin(); if (SSceneOutliner* SceneOutliner = SceneOutlinerPtr.Get()) { if (const ISceneOutlinerTreeItem* ItemPtr = Item.Pin().Get()) { return SceneOutliner->OnDragOverItem(DragDropEvent, *ItemPtr); } return FReply::Unhandled(); } return FReply::Handled(); } #undef LOCTEXT_NAMESPACE