Files
UnrealEngine/Engine/Source/Editor/Sequencer/Private/Tools/SequencerEditTool_Selection.cpp
2025-05-18 13:04:45 +08:00

657 lines
21 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Tools/SequencerEditTool_Selection.h"
#include "Styling/AppStyle.h"
#include "SequencerCommonHelpers.h"
#include "SSequencer.h"
#include "ISequencerEditTool.h"
#include "ISequencerSection.h"
#include "SequencerHotspots.h"
#include "IKeyArea.h"
#include "Tools/SequencerEntityVisitor.h"
#include "SequencerCommands.h"
#include "MVVM/Selection/Selection.h"
#include "MVVM/Views/ISequencerTreeView.h"
#include "MVVM/Views/ITrackAreaHotspot.h"
#include "MVVM/ViewModels/SequencerEditorViewModel.h"
#include "MVVM/ViewModels/VirtualTrackArea.h"
#include "MVVM/ViewModels/ViewModel.h"
#include "MVVM/ViewModels/SequenceModel.h"
#include "MVVM/Views/STrackAreaView.h"
#include "MVVM/Views/SOutlinerView.h"
#include "MVVM/Extensions/IOutlinerExtension.h"
#include "MVVM/Extensions/IPinnableExtension.h"
#include "MVVM/Extensions/ISelectableExtension.h"
struct FSelectionPreviewVisitor final
: ISequencerEntityVisitor
{
FSelectionPreviewVisitor(FSequencerSelectionPreview& InSelectionPreview, UE::Sequencer::FSequencerSelection& InSelection, ESelectionPreviewState InSetStateTo, bool bInPinned)
: SelectionPreview(InSelectionPreview)
, ExistingSelection(InSelection.KeySelection.GetSelected())
, SetStateTo(InSetStateTo)
, bPinned(bInPinned)
{
bIsControlDown = FSlateApplication::Get().GetModifierKeys().IsControlDown();
}
virtual void VisitKeys(const UE::Sequencer::TViewModelPtr<UE::Sequencer::FChannelModel>& Channel, const TRange<FFrameNumber>& VisitRangeFrames) const override
{
using namespace UE::Sequencer;
TViewModelPtr<IOutlinerExtension> OutlinerItem = Channel->GetLinkedOutlinerItem();
TSharedPtr<IPinnableExtension> PinnableItem = OutlinerItem.ImplicitCast();
const bool bIsItemPinned = (PinnableItem && PinnableItem->IsPinned()) || (OutlinerItem && OutlinerItem->ShouldAnchorToTop());
if (bIsItemPinned != bPinned)
{
return;
}
KeyHandlesScratch.Reset();
Channel->GetKeyArea()->GetKeyInfo(&KeyHandlesScratch, nullptr, VisitRangeFrames);
for (int32 Index = 0; Index < KeyHandlesScratch.Num(); ++Index)
{
// Under default behavior keys have priority, so if a key is changing selection state then we remove any sections from the selection. The user can bypass this
// by holding down the control key which will allow selecting both keys and sections.
bool bKeySelectionHasPriority = !bIsControlDown;
bool bKeyIsSelected = ExistingSelection.Contains(KeyHandlesScratch[Index]);
if (bKeySelectionHasPriority &&
((bKeyIsSelected && SetStateTo == ESelectionPreviewState::NotSelected) ||
(!bKeyIsSelected && SetStateTo == ESelectionPreviewState::Selected)))
{
// Clear selected models
SelectionPreview.EmptyDefinedModelStates();
}
SelectionPreview.SetSelectionState(Channel, KeyHandlesScratch[Index], SetStateTo);
}
}
virtual void VisitDataModel(UE::Sequencer::FViewModel* DataModel) const override
{
using namespace UE::Sequencer;
ISelectableExtension* Selectable = DataModel->CastThis<ISelectableExtension>();
if (!Selectable || !EnumHasAnyFlags(Selectable->IsSelectable(), ESelectionIntent::PersistentSelection))
{
return;
}
// If key selection has priority then we check to see if there are any keys selected. If there are key selected, we don't add this section.
// Otherwise, we bypass this check
bool bKeySelectionHasPriority = !FSlateApplication::Get().GetModifierKeys().IsControlDown();
bool bKeyStateCheck = bKeySelectionHasPriority ? SelectionPreview.GetDefinedKeyStates().Num() == 0 : true;
if (bKeyStateCheck)
{
SelectionPreview.SetSelectionState(DataModel->AsShared(), SetStateTo);
}
}
private:
FSequencerSelectionPreview& SelectionPreview;
const TSet<FKeyHandle>& ExistingSelection;
mutable TArray<FKeyHandle> KeyHandlesScratch;
ESelectionPreviewState SetStateTo;
bool bPinned;
bool bIsControlDown;
};
class FScrubTimeDragOperation
: public UE::Sequencer::ISequencerEditToolDragOperation
{
public:
FScrubTimeDragOperation(FSequencer& InSequencer, UE::Sequencer::STrackAreaView& InTrackArea)
: Sequencer(InSequencer)
{}
public:
// ISequencerEditToolDragOperation interface
virtual FCursorReply GetCursor() const override
{
return FCursorReply::Cursor(EMouseCursor::Default);
}
virtual void OnBeginDrag(const FPointerEvent& MouseEvent, FVector2D LocalMousePos, const UE::Sequencer::FVirtualTrackArea& VirtualTrackArea) override
{
// Start a new marquee selection
InitialPosition = CurrentPosition = VirtualTrackArea.PhysicalToVirtual(LocalMousePos);
CurrentMousePos = LocalMousePos;
Sequencer.SetPlaybackStatus(EMovieScenePlayerStatus::Scrubbing);
SequencerStartTime = Sequencer.GetLocalTime();
InitialTime = VirtualTrackArea.PixelToSeconds(LocalMousePos.X);
}
virtual void OnDrag(const FPointerEvent& MouseEvent, FVector2D LocalMousePos, const UE::Sequencer::FVirtualTrackArea& VirtualTrackArea) override
{
CurrentTime = VirtualTrackArea.PixelToSeconds(LocalMousePos.X);
FFrameTime FrameTime = CalculateScrubTime();
Sequencer.SnapSequencerTime(FrameTime);
Sequencer.SetLocalTimeDirectly(FrameTime, true);
}
virtual void OnEndDrag(const FPointerEvent& MouseEvent, FVector2D LocalMousePos, const UE::Sequencer::FVirtualTrackArea& VirtualTrackArea) override
{
Sequencer.SetPlaybackStatus(EMovieScenePlayerStatus::Stopped);
}
virtual int32 OnPaint(const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId) const override
{
return LayerId;
}
private:
FFrameTime CalculateScrubTime()
{
double Diff = CurrentTime - InitialTime;
FFrameTime FrameTime= SequencerStartTime.Rate.AsFrameTime(Diff);
FrameTime += SequencerStartTime.Time;
return FrameTime;
}
/** The sequencer itself */
FSequencer& Sequencer;
FVector2D InitialPosition;
FVector2D CurrentPosition;
FVector2D CurrentMousePos;
FQualifiedFrameTime SequencerStartTime;
double InitialTime;
double CurrentTime;
};
class FMarqueeDragOperation
: public UE::Sequencer::ISequencerEditToolDragOperation
{
public:
FMarqueeDragOperation(FSequencer& InSequencer, UE::Sequencer::STrackAreaView& InTrackArea)
: Sequencer(InSequencer)
, TrackArea(InTrackArea)
, SequencerWidget(StaticCastSharedRef<SSequencer>(InSequencer.GetSequencerWidget()))
, PreviewState(ESelectionPreviewState::Selected)
{}
public:
// ISequencerEditToolDragOperation interface
virtual FCursorReply GetCursor() const override
{
return FCursorReply::Cursor( EMouseCursor::Default );
}
virtual void OnBeginDrag(const FPointerEvent& MouseEvent, FVector2D LocalMousePos, const UE::Sequencer::FVirtualTrackArea& VirtualTrackArea) override
{
// Start a new marquee selection
InitialPosition = CurrentPosition = VirtualTrackArea.PhysicalToVirtual(LocalMousePos);
CurrentMousePos = LocalMousePos;
EventSuppressor = Sequencer.GetViewModel()->GetSelection()->SuppressEventsLongRunning();
if (MouseEvent.IsShiftDown())
{
PreviewState = ESelectionPreviewState::Selected;
}
else if (MouseEvent.IsAltDown())
{
PreviewState = ESelectionPreviewState::NotSelected;
}
else
{
PreviewState = ESelectionPreviewState::Selected;
// @todo: selection in transactions
//leave selections in the tree view alone so that dragging operations act similarly to click operations which don't change treeview selection state.
Sequencer.GetViewModel()->GetSelection()->KeySelection.Empty();
Sequencer.GetViewModel()->GetSelection()->TrackArea.Empty();
}
}
virtual void OnDrag(const FPointerEvent& MouseEvent, FVector2D LocalMousePos, const UE::Sequencer::FVirtualTrackArea& VirtualTrackArea) override
{
using namespace UE::Sequencer;
// hange the current marquee selection
const FVector2D MouseDelta = MouseEvent.GetCursorDelta();
// Handle virtual scrolling when at the vertical extremes of the widget (performed before we clamp the mouse pos)
{
const float ScrollThresholdV = VirtualTrackArea.GetPhysicalSize().Y * 0.025f;
TSharedPtr<SOutlinerView> TreeView = TrackArea.GetOutliner().Pin();
float Difference = LocalMousePos.Y - ScrollThresholdV;
if (Difference < 0 && MouseDelta.Y < 0)
{
TreeView->ScrollByDelta( Difference * 0.1f );
}
Difference = LocalMousePos.Y - (VirtualTrackArea.GetPhysicalSize().Y - ScrollThresholdV);
if (Difference > 0 && MouseDelta.Y > 0)
{
TreeView->ScrollByDelta( Difference * 0.1f) ;
}
}
// Clamp the vertical position to the actual bounds of the track area
LocalMousePos.Y = FMath::Clamp(LocalMousePos.Y, 0.f, VirtualTrackArea.GetPhysicalSize().Y);
CurrentPosition = VirtualTrackArea.PhysicalToVirtual(LocalMousePos);
// Clamp software cursor position to bounds of the track area
CurrentMousePos = LocalMousePos;
CurrentMousePos.X = FMath::Clamp(CurrentMousePos.X, 0.f, VirtualTrackArea.GetPhysicalSize().X);
TRange<double> ViewRange = Sequencer.GetViewRange();
// Handle virtual scrolling when at the horizontal extremes of the widget
{
const double ScrollThresholdH = ViewRange.Size<double>() * 0.025f;
double Difference = CurrentPosition.X - (ViewRange.GetLowerBoundValue() + ScrollThresholdH);
if (Difference < 0 && MouseDelta.X < 0)
{
Sequencer.StartAutoscroll(Difference);
}
else
{
Difference = CurrentPosition.X - (ViewRange.GetUpperBoundValue() - ScrollThresholdH);
if (Difference > 0 && MouseDelta.X > 0)
{
Sequencer.StartAutoscroll(Difference);
}
else
{
Sequencer.StopAutoscroll();
}
}
}
// Calculate the size of a key in virtual space
FVector2D VirtualKeySize;
VirtualKeySize.X = SequencerSectionConstants::KeySize.X / VirtualTrackArea.GetPhysicalSize().X * ViewRange.Size<float>();
// Vertically, virtual units == physical units
VirtualKeySize.Y = SequencerSectionConstants::KeySize.Y;
// Visit everything using the preview selection primarily as well as the
FSequencerSelectionPreview& SelectionPreview = Sequencer.GetSelectionPreview();
// Ensure the preview is empty before calculating the intersection
SelectionPreview.Empty();
// Now walk everything within the current marquee range, setting preview selection states as we go
FSequencerEntityWalker Walker(FSequencerEntityRange(TopLeft(), BottomRight(), VirtualTrackArea.GetTickResolution()), VirtualKeySize);
Walker.Traverse(FSelectionPreviewVisitor(SelectionPreview, *Sequencer.GetViewModel()->GetSelection(), PreviewState, TrackArea.ShowPinned()), Sequencer.GetViewModel()->GetRootModel());
}
virtual void OnEndDrag(const FPointerEvent& MouseEvent, FVector2D LocalMousePos, const UE::Sequencer::FVirtualTrackArea& VirtualTrackArea) override
{
using namespace UE::Sequencer;
// finish dragging the marquee selection
FSequencerSelection& Selection = *Sequencer.GetViewModel()->GetSelection();
FSequencerSelectionPreview& SelectionPreview = Sequencer.GetSelectionPreview();
// Patch everything from the selection preview into the actual selection
for (const TPair<FKeyHandle, ESelectionPreviewState>& Pair : SelectionPreview.GetDefinedKeyStates())
{
if (Pair.Value == ESelectionPreviewState::Selected)
{
Selection.KeySelection.Select(SelectionPreview.GetChannelForKey(Pair.Key), Pair.Key);
}
else
{
Selection.KeySelection.Deselect(Pair.Key);
}
}
for (const TPair<TWeakPtr<FViewModel>, ESelectionPreviewState>& Pair : SelectionPreview.GetDefinedModelStates())
{
if (TSharedPtr<FViewModel> Model = Pair.Key.Pin())
{
if (TViewModelPtr<IOutlinerExtension> OutlinerItem = CastViewModel<IOutlinerExtension>(Model))
{
Selection.Outliner.Select(OutlinerItem);
}
else
{
Selection.TrackArea.Select(Model);
}
}
}
// Broadcast selection events
EventSuppressor = nullptr;
// We're done with this now
SelectionPreview.Empty();
}
virtual int32 OnPaint(const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId) const override
{
using namespace UE::Sequencer;
// convert to physical space for rendering
const FVirtualTrackArea VirtualTrackArea = SequencerWidget->GetVirtualTrackArea(&TrackArea);
FVector2D SelectionTopLeft = VirtualTrackArea.VirtualToPhysical(TopLeft());
FVector2D SelectionBottomRight = VirtualTrackArea.VirtualToPhysical(BottomRight());
FSlateDrawElement::MakeBox(
OutDrawElements,
LayerId,
AllottedGeometry.ToPaintGeometry(SelectionBottomRight - SelectionTopLeft, FSlateLayoutTransform(SelectionTopLeft)),
FAppStyle::GetBrush(TEXT("MarqueeSelection"))
);
return LayerId + 1;
}
private:
FVector2D TopLeft() const
{
return FVector2D(
FMath::Min(InitialPosition.X, CurrentPosition.X),
FMath::Min(InitialPosition.Y, CurrentPosition.Y)
);
}
FVector2D BottomRight() const
{
return FVector2D(
FMath::Max(InitialPosition.X, CurrentPosition.X),
FMath::Max(InitialPosition.Y, CurrentPosition.Y)
);
}
/** The sequencer itself */
FSequencer& Sequencer;
UE::Sequencer::STrackAreaView& TrackArea;
TUniquePtr<UE::Sequencer::FSelectionEventSuppressor> EventSuppressor;
/** Sequencer widget */
TSharedRef<SSequencer> SequencerWidget;
/** Whether we should select/deselect things in this marquee operation */
ESelectionPreviewState PreviewState;
FVector2D InitialPosition;
FVector2D CurrentPosition;
FVector2D CurrentMousePos;
};
const FName FSequencerEditTool_Selection::Identifier = "Selection";
FSequencerEditTool_Selection::FSequencerEditTool_Selection(FSequencer& InSequencer, UE::Sequencer::STrackAreaView& InTrackArea)
: FSequencerEditTool(InSequencer)
, TrackArea(InTrackArea)
, CursorDecorator(nullptr)
{ }
FCursorReply FSequencerEditTool_Selection::OnCursorQuery(const FGeometry& MyGeometry, const FPointerEvent& CursorEvent) const
{
if (DragOperation)
{
return DragOperation->GetCursor();
}
return FCursorReply::Cursor(EMouseCursor::Crosshairs);
}
int32 FSequencerEditTool_Selection::OnPaint(const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId) const
{
if (DragOperation.IsValid())
{
LayerId = DragOperation->OnPaint(AllottedGeometry, MyCullingRect, OutDrawElements, LayerId);
}
if (CursorDecorator)
{
FSlateDrawElement::MakeBox(
OutDrawElements,
++LayerId,
AllottedGeometry.ToPaintGeometry(CursorDecorator->ImageSize, FSlateLayoutTransform(MousePosition + FVector2D(5.f, 5.f))),
CursorDecorator
);
}
return LayerId;
}
FReply FSequencerEditTool_Selection::OnMouseButtonDown(SWidget& OwnerWidget, const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
using namespace UE::Sequencer;
UpdateCursor(MyGeometry, MouseEvent);
DelayedDrag.Reset();
if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton)
{
TSharedPtr<FSequencerEditorViewModel> SequencerViewModel = Sequencer.GetViewModel()->CastThisShared<FSequencerEditorViewModel>();
TSharedPtr<ITrackAreaHotspot> Hotspot = SequencerViewModel->GetHotspot();
DelayedDrag = FDelayedDrag_Hotspot(MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()), EKeys::LeftMouseButton, Hotspot);
return FReply::Handled();
}
return FReply::Unhandled();
}
FReply FSequencerEditTool_Selection::OnMouseMove(SWidget& OwnerWidget, const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
using namespace UE::Sequencer;
UpdateCursor(MyGeometry, MouseEvent);
if (DelayedDrag.IsSet())
{
FReply Reply = FReply::Handled();
TSharedRef<SSequencer> SequencerWidget = StaticCastSharedRef<SSequencer>(Sequencer.GetSequencerWidget());
const FVirtualTrackArea VirtualTrackArea = SequencerWidget->GetVirtualTrackArea(&TrackArea);
if (DragOperation.IsValid())
{
FVector2D LocalPosition = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition());
DragOperation->OnDrag(MouseEvent, LocalPosition, VirtualTrackArea );
}
else if (DelayedDrag->AttemptDragStart(MouseEvent))
{
if (DelayedDrag->Hotspot.IsValid())
{
// We only allow resizing with the marquee selection tool enabled
if (DelayedDrag->Hotspot->CastThis<FSectionHotspot>() == nullptr && DelayedDrag->Hotspot->CastThis<FKeyHotspot>() == nullptr)
{
DragOperation = DelayedDrag->Hotspot->InitiateDrag(MouseEvent);
}
}
if (!DragOperation.IsValid())
{
if (bIsScrubbingTime)
{
DragOperation = MakeShareable(new FScrubTimeDragOperation(Sequencer, TrackArea));
}
else
{
DragOperation = MakeShareable(new FMarqueeDragOperation(Sequencer, TrackArea));
}
}
if (DragOperation.IsValid())
{
DragOperation->OnBeginDrag(MouseEvent, DelayedDrag->GetInitialPosition(), VirtualTrackArea);
// Steal the capture, as we're now the authoritative widget in charge of a mouse-drag operation
Reply.CaptureMouse(OwnerWidget.AsShared());
}
}
return Reply;
}
return FReply::Unhandled();
}
FReply FSequencerEditTool_Selection::OnMouseButtonUp(SWidget& OwnerWidget, const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
UpdateCursor(MyGeometry, MouseEvent);
DelayedDrag.Reset();
if (DragOperation.IsValid())
{
TSharedRef<SSequencer> SequencerWidget = StaticCastSharedRef<SSequencer>(Sequencer.GetSequencerWidget());
FVector2D LocalPosition = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition());
DragOperation->OnEndDrag(MouseEvent, LocalPosition, SequencerWidget->GetVirtualTrackArea(&TrackArea));
DragOperation = nullptr;
CursorDecorator = nullptr;
Sequencer.StopAutoscroll();
return FReply::Handled().ReleaseMouseCapture();
}
else
{
SequencerHelpers::PerformDefaultSelection(Sequencer, MouseEvent);
if (MouseEvent.GetEffectingButton() == EKeys::RightMouseButton && !Sequencer.IsReadOnly())
{
TSharedPtr<SWidget> MenuContent = SequencerHelpers::SummonContextMenu( Sequencer, MyGeometry, MouseEvent );
if (MenuContent.IsValid())
{
FWidgetPath WidgetPath = MouseEvent.GetEventPath() != nullptr ? *MouseEvent.GetEventPath() : FWidgetPath();
FSlateApplication::Get().PushMenu(
OwnerWidget.AsShared(),
WidgetPath,
MenuContent.ToSharedRef(),
MouseEvent.GetScreenSpacePosition(),
FPopupTransitionEffect( FPopupTransitionEffect::ContextMenu )
);
return FReply::Handled().SetUserFocus(MenuContent.ToSharedRef(), EFocusCause::SetDirectly).ReleaseMouseCapture();
}
}
return FReply::Handled();
}
}
void FSequencerEditTool_Selection::OnMouseLeave(SWidget& OwnerWidget, const FPointerEvent& MouseEvent)
{
if (!DragOperation.IsValid())
{
CursorDecorator = nullptr;
}
bIsScrubbingTime = false;
}
void FSequencerEditTool_Selection::OnMouseCaptureLost()
{
// Delaying nulling out until next tick because this could be invoked during OnMouseMove()
GEditor->GetTimerManager()->SetTimerForNextTick([this]()
{
DelayedDrag.Reset();
DragOperation = nullptr;
CursorDecorator = nullptr;
bIsScrubbingTime = false;
});
}
bool FSequencerEditTool_Selection::IsScrubTimeKeyEvent(const FKeyEvent& InKeyEvent)
{
const FSequencerCommands& Commands = FSequencerCommands::Get();
// Need to iterate through primary and secondary to make sure they are all pressed.
for (uint32 i = 0; i < static_cast<uint8>(EMultipleKeyBindingIndex::NumChords); ++i)
{
EMultipleKeyBindingIndex ChordIndex = static_cast<EMultipleKeyBindingIndex>(i);
const FInputChord& Chord = *Commands.ScrubTimeViewport->GetActiveChord(ChordIndex);
const bool bIsMovingTimeSlider = Chord.IsValidChord() && InKeyEvent.GetKey() == Chord.Key;
if (bIsMovingTimeSlider)
{
return true;
}
}
return false;
}
FReply FSequencerEditTool_Selection::OnKeyDown(SWidget& OwnerWidget, const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent)
{
if (IsScrubTimeKeyEvent(InKeyEvent))
{
bIsScrubbingTime = true;
return FReply::Handled();
}
return FReply::Unhandled();
}
FReply FSequencerEditTool_Selection::OnKeyUp(SWidget& OwnerWidget, const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent)
{
if (IsScrubTimeKeyEvent(InKeyEvent) && bIsScrubbingTime)
{
bIsScrubbingTime = false;
//would be nice to cancle the drag but doesn't seem like we can like we can with curve editor drag handlers.
return FReply::Handled();
}
return FReply::Unhandled();
}
FName FSequencerEditTool_Selection::GetIdentifier() const
{
return Identifier;
}
bool FSequencerEditTool_Selection::CanDeactivate() const
{
return !DelayedDrag.IsSet();
}
TSharedPtr<UE::Sequencer::ITrackAreaHotspot> FSequencerEditTool_Selection::GetDragHotspot() const
{
return DelayedDrag.IsSet() ? DelayedDrag->Hotspot : nullptr;
}
void FSequencerEditTool_Selection::UpdateCursor(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
MousePosition = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition());
// Don't update the brush if we have a drag operation
if (!DragOperation.IsValid())
{
if (MouseEvent.IsShiftDown())
{
CursorDecorator = FAppStyle::Get().GetBrush(TEXT("Sequencer.CursorDecorator_MarqueeAdd"));
}
else if (MouseEvent.IsAltDown())
{
CursorDecorator = FAppStyle::Get().GetBrush(TEXT("Sequencer.CursorDecorator_MarqueeSubtract"));
}
else
{
CursorDecorator = nullptr;
}
}
}