// Copyright Epic Games, Inc. All Rights Reserved. #include "Components/WidgetInteractionComponent.h" #include "UMGPrivate.h" #include "CollisionQueryParams.h" #include "Components/PrimitiveComponent.h" #include "Engine/GameViewportClient.h" #include "Kismet/KismetSystemLibrary.h" #include "Components/ArrowComponent.h" #include "Framework/Application/SlateApplication.h" #include "Framework/Application/SlateUser.h" #include "Kismet/GameplayStatics.h" #include "DrawDebugHelpers.h" #include "GenericPlatform/GenericPlatformInputDeviceMapper.h" #include "Components/WidgetComponent.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(WidgetInteractionComponent) #define LOCTEXT_NAMESPACE "WidgetInteraction" UWidgetInteractionComponent::UWidgetInteractionComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) , VirtualUserIndex(0) , PointerIndex(0) , InteractionDistance(500) , InteractionSource(EWidgetInteractionSource::World) , bEnableHitTesting(true) , bShowDebug(false) , DebugSphereLineThickness(2.f) , DebugLineThickness(1.f) , DebugColor(FLinearColor::Red) { PrimaryComponentTick.bCanEverTick = true; PrimaryComponentTick.bTickEvenWhenPaused = true; TraceChannel = ECC_Visibility; bAutoActivate = true; #if WITH_EDITORONLY_DATA ArrowComponent = ObjectInitializer.CreateEditorOnlyDefaultSubobject(this, TEXT("ArrowComponent0")); if ( ArrowComponent && !IsTemplate() ) { ArrowComponent->ArrowColor = DebugColor.ToFColor(true); ArrowComponent->AttachToComponent(this, FAttachmentTransformRules(EAttachmentRule::KeepRelative, false)); } #endif } void UWidgetInteractionComponent::OnComponentCreated() { Super::OnComponentCreated(); #if WITH_EDITORONLY_DATA if ( ArrowComponent ) { ArrowComponent->ArrowColor = DebugColor.ToFColor(true); ArrowComponent->SetVisibility(bEnableHitTesting); } #endif } void UWidgetInteractionComponent::Activate(bool bReset) { Super::Activate(bReset); // Only create another user in a real world. FindOrCreateVirtualUser changes focus if ( FSlateApplication::IsInitialized() && !GetWorld()->IsPreviewWorld()) { if ( !VirtualUser.IsValid() ) { VirtualUser = FSlateApplication::Get().FindOrCreateVirtualUser(VirtualUserIndex); } } } void UWidgetInteractionComponent::Deactivate() { Super::Deactivate(); if ( FSlateApplication::IsInitialized() ) { if ( VirtualUser.IsValid() ) { FSlateApplication::Get().UnregisterUser(VirtualUser->GetUserIndex()); VirtualUser.Reset(); } } } void UWidgetInteractionComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); SimulatePointerMovement(); } bool UWidgetInteractionComponent::CanSendInput() { return FSlateApplication::IsInitialized() && VirtualUser.IsValid(); } void UWidgetInteractionComponent::SetCustomHitResult(const FHitResult& HitResult) { CustomHitResult = HitResult; } void UWidgetInteractionComponent::SetFocus(UWidget* FocusWidget) { if (VirtualUser.IsValid()) { FSlateApplication::Get().SetUserFocus(VirtualUser->GetUserIndex(), FocusWidget->GetCachedWidget(), EFocusCause::SetDirectly); } } FWidgetPath UWidgetInteractionComponent::FindHoveredWidgetPath(const FWidgetTraceResult& TraceResult) const { if (TraceResult.HitWidgetComponent) { return FWidgetPath(TraceResult.HitWidgetComponent->GetHitWidgetPath(TraceResult.LocalHitLocation, /*bIgnoreEnabledStatus*/ false)); } else { return FWidgetPath(); } } UWidgetInteractionComponent::FWidgetTraceResult UWidgetInteractionComponent::PerformTrace() const { FWidgetTraceResult TraceResult; TArray MultiHits; FVector WorldDirection = FVector::ZeroVector; switch( InteractionSource ) { case EWidgetInteractionSource::World: { const FVector WorldLocation = GetComponentLocation(); const FTransform WorldTransform = GetComponentTransform(); WorldDirection = WorldTransform.GetUnitAxis(EAxis::X); TArray PrimitiveChildren; GetRelatedComponentsToIgnoreInAutomaticHitTesting(PrimitiveChildren); FCollisionQueryParams Params(SCENE_QUERY_STAT(WidgetInteractionComponentTrace)); Params.AddIgnoredComponents(PrimitiveChildren); TraceResult.LineStartLocation = WorldLocation; TraceResult.LineEndLocation = WorldLocation + (WorldDirection * InteractionDistance); GetWorld()->LineTraceMultiByChannel(MultiHits, TraceResult.LineStartLocation, TraceResult.LineEndLocation, TraceChannel, Params); break; } case EWidgetInteractionSource::Mouse: case EWidgetInteractionSource::CenterScreen: { TArray PrimitiveChildren; GetRelatedComponentsToIgnoreInAutomaticHitTesting(PrimitiveChildren); FCollisionQueryParams Params(SCENE_QUERY_STAT(WidgetInteractionComponentTrace)); Params.AddIgnoredComponents(PrimitiveChildren); const UWorld* World = GetWorld(); APlayerController* PlayerController = World ? World->GetFirstPlayerController():nullptr; if (!PlayerController) { UE_LOG(LogUMG, Warning, TEXT("Widget Interaction Component cannot perform trace without a valid PlayerController.")); return FWidgetTraceResult(); } ULocalPlayer* LocalPlayer = PlayerController->GetLocalPlayer(); if ( LocalPlayer && LocalPlayer->ViewportClient ) { if ( InteractionSource == EWidgetInteractionSource::Mouse ) { FVector2D MousePosition; if ( LocalPlayer->ViewportClient->GetMousePosition(MousePosition) ) { FVector WorldOrigin; if ( UGameplayStatics::DeprojectScreenToWorld(PlayerController, MousePosition, WorldOrigin, WorldDirection) == true ) { TraceResult.LineStartLocation = WorldOrigin; TraceResult.LineEndLocation = WorldOrigin + WorldDirection * InteractionDistance; GetWorld()->LineTraceMultiByChannel(MultiHits, TraceResult.LineStartLocation, TraceResult.LineEndLocation, TraceChannel, Params); } } } else if ( InteractionSource == EWidgetInteractionSource::CenterScreen ) { FVector2D ViewportSize; LocalPlayer->ViewportClient->GetViewportSize(ViewportSize); FVector WorldOrigin; if ( UGameplayStatics::DeprojectScreenToWorld(PlayerController, ViewportSize * 0.5f, WorldOrigin, WorldDirection) == true ) { TraceResult.LineStartLocation = WorldOrigin; TraceResult.LineEndLocation = WorldOrigin + WorldDirection * InteractionDistance; GetWorld()->LineTraceMultiByChannel(MultiHits, WorldOrigin, WorldOrigin + WorldDirection * InteractionDistance, TraceChannel, Params); } } } break; } case EWidgetInteractionSource::Custom: { WorldDirection = (CustomHitResult.TraceEnd - CustomHitResult.TraceStart).GetSafeNormal(); TraceResult.HitResult = CustomHitResult; TraceResult.bWasHit = CustomHitResult.bBlockingHit; TraceResult.LineStartLocation = CustomHitResult.TraceStart; TraceResult.LineEndLocation = CustomHitResult.TraceEnd; break; } } // If it's not a custom interaction, we do some custom filtering to ignore invisible widgets. if ( InteractionSource != EWidgetInteractionSource::Custom ) { for ( const FHitResult& HitResult : MultiHits ) { if ( UWidgetComponent* HitWidgetComponent = Cast(HitResult.GetComponent()) ) { if ( HitWidgetComponent->IsVisible() ) { TraceResult.bWasHit = true; TraceResult.HitResult = HitResult; break; } } else if (HitResult.bBlockingHit) { // If we hit something that wasn't a widget component, we're done. break; } } } // Resolve trace to location on widget. if (TraceResult.bWasHit) { TraceResult.HitWidgetComponent = Cast(TraceResult.HitResult.GetComponent()); if (TraceResult.HitWidgetComponent) { // @todo WASTED WORK: GetLocalHitLocation() gets called in GetHitWidgetPath(); if (TraceResult.HitWidgetComponent->GetGeometryMode() == EWidgetGeometryMode::Cylinder) { TTuple CylinderHitLocation = TraceResult.HitWidgetComponent->GetCylinderHitLocation(TraceResult.HitResult.ImpactPoint, WorldDirection); TraceResult.HitResult.ImpactPoint = CylinderHitLocation.Get<0>(); TraceResult.LocalHitLocation = CylinderHitLocation.Get<1>(); } else { ensure(TraceResult.HitWidgetComponent->GetGeometryMode() == EWidgetGeometryMode::Plane); TraceResult.HitWidgetComponent->GetLocalHitLocation(TraceResult.HitResult.ImpactPoint, TraceResult.LocalHitLocation); } TraceResult.HitWidgetPath = FindHoveredWidgetPath(TraceResult); } } return TraceResult; } void UWidgetInteractionComponent::GetRelatedComponentsToIgnoreInAutomaticHitTesting(TArray& IgnorePrimitives) const { TArray SceneChildren; if ( AActor* Owner = GetOwner() ) { if ( USceneComponent* Root = Owner->GetRootComponent() ) { Root = Root->GetAttachmentRoot(); Root->GetChildrenComponents(true, SceneChildren); SceneChildren.Add(Root); } } for ( USceneComponent* SceneComponent : SceneChildren ) { if ( UPrimitiveComponent* PrimtiveComponet = Cast(SceneComponent) ) { // Don't ignore widget components that are siblings. if ( SceneComponent->IsA() ) { continue; } IgnorePrimitives.Add(PrimtiveComponet); } } } bool UWidgetInteractionComponent::CanInteractWithComponent(UWidgetComponent* Component) const { bool bCanInteract = false; if (Component) { bCanInteract = !GetWorld()->IsPaused() || Component->PrimaryComponentTick.bTickEvenWhenPaused; } return bCanInteract; } FWidgetPath UWidgetInteractionComponent::DetermineWidgetUnderPointer() { FWidgetPath WidgetPathUnderPointer; bIsHoveredWidgetInteractable = false; bIsHoveredWidgetFocusable = false; bIsHoveredWidgetHitTestVisible = false; UWidgetComponent* OldHoveredWidgetComponent = GetHoveredWidgetComponent(); WeakHoveredWidgetComponent.Reset(); FWidgetTraceResult TraceResult = PerformTrace(); LastHitResult = TraceResult.HitResult; WeakHoveredWidgetComponent = TraceResult.HitWidgetComponent; LastLocalHitLocation = LocalHitLocation; LocalHitLocation = TraceResult.bWasHit ? TraceResult.LocalHitLocation : LastLocalHitLocation; WidgetPathUnderPointer = TraceResult.HitWidgetPath; UWidgetComponent* NewHoveredWidgetComponent = GetHoveredWidgetComponent(); #if ENABLE_DRAW_DEBUG if ( bShowDebug ) { if ( NewHoveredWidgetComponent ) { UKismetSystemLibrary::DrawDebugSphere(this, LastHitResult.ImpactPoint, 2.5f, 12, DebugColor, 0, DebugSphereLineThickness); } if ( InteractionSource == EWidgetInteractionSource::World || InteractionSource == EWidgetInteractionSource::Custom ) { if ( NewHoveredWidgetComponent ) { UKismetSystemLibrary::DrawDebugLine(this, LastHitResult.TraceStart, LastHitResult.ImpactPoint, DebugColor, 0, DebugLineThickness); } else { UKismetSystemLibrary::DrawDebugLine(this, TraceResult.LineStartLocation, TraceResult.LineEndLocation, DebugColor, 0, DebugLineThickness); } } } #endif // ENABLE_DRAW_DEBUG if ( NewHoveredWidgetComponent ) { NewHoveredWidgetComponent->RequestRedraw(); } if ( WidgetPathUnderPointer.IsValid() ) { const FArrangedChildren::FArrangedWidgetArray& AllArrangedWidgets = WidgetPathUnderPointer.Widgets.GetInternalArray(); for ( const FArrangedWidget& ArrangedWidget : AllArrangedWidgets ) { const TSharedRef& Widget = ArrangedWidget.Widget; if ( Widget->IsEnabled() ) { if ( Widget->IsInteractable() ) { bIsHoveredWidgetInteractable = true; } if ( Widget->SupportsKeyboardFocus() ) { bIsHoveredWidgetFocusable = true; } } if ( Widget->GetVisibility().IsHitTestVisible() ) { bIsHoveredWidgetHitTestVisible = true; } } } if ( NewHoveredWidgetComponent != OldHoveredWidgetComponent ) { if ( OldHoveredWidgetComponent ) { OldHoveredWidgetComponent->RequestRedraw(); } OnHoveredWidgetChanged.Broadcast( NewHoveredWidgetComponent, OldHoveredWidgetComponent ); } return WidgetPathUnderPointer; } void UWidgetInteractionComponent::SimulatePointerMovement() { if ( !bEnableHitTesting ) { return; } if ( !CanSendInput() ) { return; } FWidgetPath WidgetPathUnderFinger = DetermineWidgetUnderPointer(); ensure(PointerIndex >= 0); FPointerEvent PointerEvent( VirtualUser->GetUserIndex(), (uint32)PointerIndex, LocalHitLocation, LastLocalHitLocation, PressedKeys, FKey(), 0.0f, ModifierKeys); if (WidgetPathUnderFinger.IsValid()) { check(WeakHoveredWidgetComponent.IsValid()); LastWidgetPath = WidgetPathUnderFinger; FSlateApplication::Get().RoutePointerMoveEvent(WidgetPathUnderFinger, PointerEvent, false); } else { FWidgetPath EmptyWidgetPath; FSlateApplication::Get().RoutePointerMoveEvent(EmptyWidgetPath, PointerEvent, false); LastWidgetPath = FWeakWidgetPath(); } } void UWidgetInteractionComponent::PressPointerKey(FKey Key) { if ( !CanSendInput() ) { return; } if (PressedKeys.Contains(Key)) { return; } PressedKeys.Add(Key); if ( !LastWidgetPath.IsValid() ) { // If the cached widget path isn't valid, attempt to find a valid widget since we might have received a touch input LastWidgetPath = DetermineWidgetUnderPointer(); } FWidgetPath WidgetPathUnderFinger = LastWidgetPath.ToWidgetPath(); ensure(PointerIndex >= 0); FPointerEvent PointerEvent; // Find the primary input device for this Slate User FInputDeviceId InputDeviceId = INPUTDEVICEID_NONE; if (TSharedPtr SlateUser = FSlateApplication::Get().GetUser(VirtualUser->GetUserIndex())) { FPlatformUserId PlatUser = SlateUser->GetPlatformUserId(); InputDeviceId = IPlatformInputDeviceMapper::Get().GetPrimaryInputDeviceForUser(PlatUser); } // Just in case there was no input device assigned to this virtual user, get the default platform // input device if (!InputDeviceId.IsValid()) { InputDeviceId = IPlatformInputDeviceMapper::Get().GetDefaultInputDevice(); } if (Key.IsTouch()) { PointerEvent = FPointerEvent( InputDeviceId, (uint32)PointerIndex, LocalHitLocation, LastLocalHitLocation, 1.0f, false, false, false, FModifierKeysState(), 0, VirtualUser->GetUserIndex()); } else { PointerEvent = FPointerEvent( InputDeviceId, (uint32)PointerIndex, LocalHitLocation, LastLocalHitLocation, PressedKeys, Key, 0.0f, ModifierKeys, VirtualUser->GetUserIndex()); } FReply Reply = FSlateApplication::Get().RoutePointerDownEvent(WidgetPathUnderFinger, PointerEvent); // @TODO Something about double click, expose directly, or automatically do it if key press happens within // the double click timeframe? //Reply = FSlateApplication::Get().RoutePointerDoubleClickEvent( WidgetPathUnderFinger, PointerEvent ); } void UWidgetInteractionComponent::ReleasePointerKey(FKey Key) { if ( !CanSendInput() ) { return; } if (!PressedKeys.Contains(Key)) { return; } PressedKeys.Remove(Key); FWidgetPath WidgetPathUnderFinger = LastWidgetPath.ToWidgetPath(); // Need to clear the widget path for cases where the component isn't ticking/clearing itself. LastWidgetPath = FWeakWidgetPath(); ensure(PointerIndex >= 0); FPointerEvent PointerEvent; // Find the primary input device for this Slate User FInputDeviceId InputDeviceId = INPUTDEVICEID_NONE; if (TSharedPtr SlateUser = FSlateApplication::Get().GetUser(VirtualUser->GetUserIndex())) { FPlatformUserId PlatUser = SlateUser->GetPlatformUserId(); InputDeviceId = IPlatformInputDeviceMapper::Get().GetPrimaryInputDeviceForUser(PlatUser); } // Just in case there was no input device assigned to this virtual user, get the default platform // input device if (!InputDeviceId.IsValid()) { InputDeviceId = IPlatformInputDeviceMapper::Get().GetDefaultInputDevice(); } if (Key.IsTouch()) { PointerEvent = FPointerEvent( InputDeviceId, (uint32)PointerIndex, LocalHitLocation, LastLocalHitLocation, 0.0f, false, false, false, FModifierKeysState(), 0, VirtualUser->GetUserIndex()); } else { PointerEvent = FPointerEvent( InputDeviceId, (uint32)PointerIndex, LocalHitLocation, LastLocalHitLocation, PressedKeys, Key, 0.0f, ModifierKeys, VirtualUser->GetUserIndex()); } FReply Reply = FSlateApplication::Get().RoutePointerUpEvent(WidgetPathUnderFinger, PointerEvent); } bool UWidgetInteractionComponent::PressKey(FKey Key, bool bRepeat) { if ( !CanSendInput() ) { return false; } bool bHasKeyCode, bHasCharCode; uint32 KeyCode, CharCode; GetKeyAndCharCodes(Key, bHasKeyCode, KeyCode, bHasCharCode, CharCode); FKeyEvent KeyEvent(Key, ModifierKeys, VirtualUser->GetUserIndex(), bRepeat, CharCode, KeyCode); bool bDownResult = FSlateApplication::Get().ProcessKeyDownEvent(KeyEvent); bool bKeyCharResult = false; if (bHasCharCode) { if (CharCode <= 0xD7FF || (CharCode >= 0xE000 && CharCode <= 0xFFFF)) // This is a valid UTF16 char from Basic Multilangual Plane { FCharacterEvent CharacterEvent(static_cast(CharCode), ModifierKeys, VirtualUser->GetUserIndex(), bRepeat); bKeyCharResult = FSlateApplication::Get().ProcessKeyCharEvent(CharacterEvent); } } return bDownResult || bKeyCharResult; } bool UWidgetInteractionComponent::ReleaseKey(FKey Key) { if ( !CanSendInput() ) { return false; } bool bHasKeyCode, bHasCharCode; uint32 KeyCode, CharCode; GetKeyAndCharCodes(Key, bHasKeyCode, KeyCode, bHasCharCode, CharCode); FKeyEvent KeyEvent(Key, ModifierKeys, VirtualUser->GetUserIndex(), false, CharCode, KeyCode); return FSlateApplication::Get().ProcessKeyUpEvent(KeyEvent); } void UWidgetInteractionComponent::GetKeyAndCharCodes(const FKey& Key, bool& bHasKeyCode, uint32& KeyCode, bool& bHasCharCode, uint32& CharCode) { const uint32* KeyCodePtr; const uint32* CharCodePtr; FInputKeyManager::Get().GetCodesFromKey(Key, KeyCodePtr, CharCodePtr); bHasKeyCode = KeyCodePtr ? true : false; bHasCharCode = CharCodePtr ? true : false; KeyCode = KeyCodePtr ? *KeyCodePtr : 0; CharCode = CharCodePtr ? *CharCodePtr : 0; // These special keys are not handled by the platform layer, and while not printable // have character mappings that several widgets look for, since the hardware sends them. if (CharCodePtr == nullptr) { if (Key == EKeys::Tab) { CharCode = '\t'; bHasCharCode = true; } else if (Key == EKeys::BackSpace) { CharCode = '\b'; bHasCharCode = true; } else if (Key == EKeys::Enter) { CharCode = '\n'; bHasCharCode = true; } } } bool UWidgetInteractionComponent::PressAndReleaseKey(FKey Key) { const bool PressResult = PressKey(Key, false); const bool ReleaseResult = ReleaseKey(Key); return PressResult || ReleaseResult; } bool UWidgetInteractionComponent::SendKeyChar(FString Characters, bool bRepeat) { if ( !CanSendInput() ) { return false; } bool bProcessResult = false; for ( int32 CharIndex = 0; CharIndex < Characters.Len(); CharIndex++ ) { TCHAR CharKey = Characters[CharIndex]; FCharacterEvent CharacterEvent(CharKey, ModifierKeys, VirtualUser->GetUserIndex(), bRepeat); bProcessResult |= FSlateApplication::Get().ProcessKeyCharEvent(CharacterEvent); } return bProcessResult; } void UWidgetInteractionComponent::ScrollWheel(float ScrollDelta) { if ( !CanSendInput() ) { return; } FWidgetPath WidgetPathUnderFinger = LastWidgetPath.ToWidgetPath(); ensure(PointerIndex >= 0); FPointerEvent MouseWheelEvent( VirtualUser->GetUserIndex(), (uint32)PointerIndex, LocalHitLocation, LastLocalHitLocation, PressedKeys, EKeys::MouseWheelAxis, ScrollDelta, ModifierKeys); FSlateApplication::Get().RouteMouseWheelOrGestureEvent(WidgetPathUnderFinger, MouseWheelEvent, nullptr); } UWidgetComponent* UWidgetInteractionComponent::GetHoveredWidgetComponent() const { return WeakHoveredWidgetComponent.Get(); } bool UWidgetInteractionComponent::IsOverInteractableWidget() const { return bIsHoveredWidgetInteractable; } bool UWidgetInteractionComponent::IsOverFocusableWidget() const { return bIsHoveredWidgetFocusable; } bool UWidgetInteractionComponent::IsOverHitTestVisibleWidget() const { return bIsHoveredWidgetHitTestVisible; } const FWeakWidgetPath& UWidgetInteractionComponent::GetHoveredWidgetPath() const { return LastWidgetPath; } const FHitResult& UWidgetInteractionComponent::GetLastHitResult() const { return LastHitResult; } FVector2D UWidgetInteractionComponent::Get2DHitLocation() const { return LocalHitLocation; } #undef LOCTEXT_NAMESPACE