diff --git a/Content/Editor/ZeusUMGForge/FontMap.uasset b/Content/Editor/ZeusUMGForge/FontMap.uasset index 7a0d518..9cdac71 100644 Binary files a/Content/Editor/ZeusUMGForge/FontMap.uasset and b/Content/Editor/ZeusUMGForge/FontMap.uasset differ diff --git a/Content/ZMMO/UI/Screens/PlayerStatus/UI_PlayerStatus_Window.uasset b/Content/ZMMO/UI/Screens/PlayerStatus/UI_PlayerStatus_Window.uasset index d06fac0..c8760d2 100644 Binary files a/Content/ZMMO/UI/Screens/PlayerStatus/UI_PlayerStatus_Window.uasset and b/Content/ZMMO/UI/Screens/PlayerStatus/UI_PlayerStatus_Window.uasset differ diff --git a/Content/ZMMO/UI/Themes/Default/Panel/Outlines/Effects/T_Panel_Outline_Effect_Square_01.uasset b/Content/ZMMO/UI/Themes/Default/Panel/Outlines/Effects/T_Panel_Outline_Effect_Square_01.uasset index 20754b6..57fd79b 100644 Binary files a/Content/ZMMO/UI/Themes/Default/Panel/Outlines/Effects/T_Panel_Outline_Effect_Square_01.uasset and b/Content/ZMMO/UI/Themes/Default/Panel/Outlines/Effects/T_Panel_Outline_Effect_Square_01.uasset differ diff --git a/Content/ZMMO/UI/Themes/Default/Panel/Outlines/T_Panel_Outline_Square_01.uasset b/Content/ZMMO/UI/Themes/Default/Panel/Outlines/T_Panel_Outline_Square_01.uasset index ae63fe1..cd9c926 100644 Binary files a/Content/ZMMO/UI/Themes/Default/Panel/Outlines/T_Panel_Outline_Square_01.uasset and b/Content/ZMMO/UI/Themes/Default/Panel/Outlines/T_Panel_Outline_Square_01.uasset differ diff --git a/Source/ZMMO/Game/UI/Common/UIDraggableWindow_Base.cpp b/Source/ZMMO/Game/UI/Common/UIDraggableWindow_Base.cpp new file mode 100644 index 0000000..a74fe50 --- /dev/null +++ b/Source/ZMMO/Game/UI/Common/UIDraggableWindow_Base.cpp @@ -0,0 +1,131 @@ +#include "UIDraggableWindow_Base.h" + +#include "Components/Border.h" +#include "Engine/Engine.h" +#include "Engine/GameViewportClient.h" +#include "Input/Reply.h" +#include "UIWindowSaveGame.h" + +void UUIDraggableWindow_Base::NativeOnActivated() +{ + Super::NativeOnActivated(); + ApplySavedPosition(); + // Validacao usa translation + viewport size, ambos disponiveis ja' aqui + // (sem precisar do CachedGeometry/layout pass). + ValidatePositionInViewport(); +} + +void UUIDraggableWindow_Base::ApplySavedPosition() +{ + const FName Id = GetWindowId(); + if (Id.IsNone()) return; + + UZMMOUISaveGame* Save = UZMMOUISaveGame::LoadOrCreate(); + FVector2D Pos; + if (Save && Save->TryGetWindowPosition(Id, Pos)) + { + SetRenderTranslation(Pos); + } +} + +void UUIDraggableWindow_Base::PersistCurrentPosition() +{ + const FName Id = GetWindowId(); + if (Id.IsNone()) return; + + UZMMOUISaveGame* Save = UZMMOUISaveGame::LoadOrCreate(); + if (!Save) return; + Save->SetWindowPosition(Id, GetRenderTransform().Translation); + UZMMOUISaveGame::SaveNow(Save); +} + +bool UUIDraggableWindow_Base::IsDragHandleHit(const FPointerEvent& MouseEvent) const +{ + if (!Border_DragHandle) return false; + const FGeometry HandleGeom = Border_DragHandle->GetCachedGeometry(); + return HandleGeom.IsUnderLocation(MouseEvent.GetScreenSpacePosition()); +} + +FReply UUIDraggableWindow_Base::NativeOnMouseButtonDown(const FGeometry& InGeometry, + const FPointerEvent& InMouseEvent) +{ + if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton && IsDragHandleHit(InMouseEvent)) + { + bMaybeDragging = true; + bDragging = false; + DragStartScreenPos = InMouseEvent.GetScreenSpacePosition(); + WidgetStartTranslation = GetRenderTransform().Translation; + return FReply::Handled().CaptureMouse(TakeWidget()); + } + return Super::NativeOnMouseButtonDown(InGeometry, InMouseEvent); +} + +FReply UUIDraggableWindow_Base::NativeOnMouseMove(const FGeometry& InGeometry, + const FPointerEvent& InMouseEvent) +{ + if (!bMaybeDragging && !bDragging) + { + return Super::NativeOnMouseMove(InGeometry, InMouseEvent); + } + + const FVector2D Delta = InMouseEvent.GetScreenSpacePosition() - DragStartScreenPos; + if (!bDragging && Delta.SizeSquared() < DragStartThresholdPx * DragStartThresholdPx) + { + return FReply::Handled(); + } + bDragging = true; + SetRenderTranslation(WidgetStartTranslation + Delta); + return FReply::Handled(); +} + +FReply UUIDraggableWindow_Base::NativeOnMouseButtonUp(const FGeometry& InGeometry, + const FPointerEvent& InMouseEvent) +{ + if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton && (bMaybeDragging || bDragging)) + { + const bool bWasRealDrag = bDragging; + bMaybeDragging = false; + bDragging = false; + if (bWasRealDrag) + { + PersistCurrentPosition(); + } + return FReply::Handled().ReleaseMouseCapture(); + } + return Super::NativeOnMouseButtonUp(InGeometry, InMouseEvent); +} + +void UUIDraggableWindow_Base::NativeOnMouseLeave(const FPointerEvent& InMouseEvent) +{ + Super::NativeOnMouseLeave(InMouseEvent); +} + +void UUIDraggableWindow_Base::ValidatePositionInViewport() +{ + if (!GEngine || !GEngine->GameViewport) return; + FVector2D ViewportSize; + GEngine->GameViewport->GetViewportSize(ViewportSize); + if (ViewportSize.IsNearlyZero()) return; + + // RenderTransform.Translation esta em coordenadas de viewport (pixels), + // independente de DPI/offset do editor — comparacao direta funciona em + // PIE e em build standalone. Assume slot centralizado (default UMG + // stack); meio-viewport e' o quanto a translation pode crescer antes + // da janela sair completamente. + const FVector2D Tr = GetRenderTransform().Translation; + const float HalfX = ViewportSize.X * 0.5f; + const float HalfY = ViewportSize.Y * 0.5f; + + const bool bOutOfBounds = + FMath::Abs(Tr.X) > HalfX - MinVisiblePixels || + FMath::Abs(Tr.Y) > HalfY - MinVisiblePixels; + + UE_LOG(LogTemp, Verbose, TEXT("DraggableWindow::Validate translation=(%.0f,%.0f) viewport=(%.0f,%.0f) outOfBounds=%d"), + Tr.X, Tr.Y, ViewportSize.X, ViewportSize.Y, bOutOfBounds ? 1 : 0); + + if (bOutOfBounds) + { + SetRenderTranslation(FVector2D::ZeroVector); + PersistCurrentPosition(); + } +} diff --git a/Source/ZMMO/Game/UI/Common/UIDraggableWindow_Base.h b/Source/ZMMO/Game/UI/Common/UIDraggableWindow_Base.h new file mode 100644 index 0000000..a24db37 --- /dev/null +++ b/Source/ZMMO/Game/UI/Common/UIDraggableWindow_Base.h @@ -0,0 +1,90 @@ +#pragma once + +#include "CoreMinimal.h" +#include "CommonActivatableWidget.h" +#include "UIDraggableWindow_Base.generated.h" + +class UBorder; +class UZMMOUISaveGame; + +/** + * Base de janelas in-game movel + persistente. Subclasse define WindowId + * (FName estavel) via override de GetWindowId(); a Base cuida do drag + * (RenderTransform.Translation) e da persistencia (UZMMOUISaveGame). + * + * Convencao do WBP: + * - Bind opcional `Border_DragHandle` cobrindo a area arrastavel (header). + * Sem ele, a janela nao captura drag (vira static). + * - `Border_DragHandle->Visibility = Visible` (precisa receber HitTest). + * + * Por que RenderTransform e nao Slot.Position? + * - Funciona em qualquer layout slot (Overlay, VBox, Canvas), nao so + * CanvasPanelSlot. + * - Nao polui o asset (translation e' transient/visual, nao layout-layout). + * + * Persistencia: na primeira ativacao, carrega a posicao salva do WindowId; + * ao soltar o drag, grava e salva o slot. + */ +UCLASS(Abstract, Blueprintable) +class ZMMO_API UUIDraggableWindow_Base : public UCommonActivatableWidget +{ + GENERATED_BODY() + +public: + /** + * Identificador estavel da janela (chave no save). Subclasse OBRIGADA + * a sobrescrever — default NAME_None desativa persistencia. + */ + virtual FName GetWindowId() const { return NAME_None; } + +protected: + virtual void NativeOnActivated() override; + virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override; + virtual FReply NativeOnMouseMove(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override; + virtual FReply NativeOnMouseButtonUp(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override; + virtual void NativeOnMouseLeave(const FPointerEvent& InMouseEvent) override; + + /** + * Define se o ponto sob o cursor pertence a area de drag. Default: usa + * Border_DragHandle (se bindado). Subclass pode override pra usar outro + * widget (ex.: Container_Header ja' existente) sem precisar adicionar + * um Border dedicado no WBP. + */ + virtual bool IsDragHandleHit(const FPointerEvent& MouseEvent) const; + + /** Aplica a posicao salva (se houver) como RenderTransform.Translation. */ + void ApplySavedPosition(); + + /** Grava a posicao atual no save e persiste no slot. */ + void PersistCurrentPosition(); + + /** + * Se a janela ficou fora do viewport (ex.: usuario arrastou pra borda e + * depois a resolucao mudou ou ele fechou e reabriu), reseta translation + * pra (0,0). Chamado no proximo tick apos Activated — cached geometry + * so' tem valor depois do primeiro layout pass. + */ + UFUNCTION() + void ValidatePositionInViewport(); + + /** Quanto da janela precisa ficar visivel pra contar como "no viewport". */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Drag") + float MinVisiblePixels = 50.f; + + /** + * Bind opcional do WBP. Apenas drags iniciados com o cursor SOBRE + * este border sao aceitos — bloqueia drag por clicar nos rows/botoes. + */ + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Drag") + TObjectPtr Border_DragHandle; + + /** Distancia minima do mouse pra confirmar "drag iniciado" (anti-jitter). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Drag") + float DragStartThresholdPx = 4.f; + +private: + bool bMaybeDragging = false; ///< pressed mas ainda nao passou do threshold + bool bDragging = false; ///< drag confirmado + FVector2D DragStartScreenPos = FVector2D::ZeroVector; + FVector2D WidgetStartTranslation = FVector2D::ZeroVector; +}; diff --git a/Source/ZMMO/Game/UI/Common/UIWindowSaveGame.cpp b/Source/ZMMO/Game/UI/Common/UIWindowSaveGame.cpp new file mode 100644 index 0000000..717778d --- /dev/null +++ b/Source/ZMMO/Game/UI/Common/UIWindowSaveGame.cpp @@ -0,0 +1,43 @@ +#include "UIWindowSaveGame.h" + +#include "Kismet/GameplayStatics.h" + +const FString UZMMOUISaveGame::SlotName = TEXT("UIPrefs"); + +UZMMOUISaveGame* UZMMOUISaveGame::LoadOrCreate() +{ + if (UGameplayStatics::DoesSaveGameExist(SlotName, UserIndex)) + { + if (UZMMOUISaveGame* Loaded = Cast( + UGameplayStatics::LoadGameFromSlot(SlotName, UserIndex))) + { + return Loaded; + } + } + return Cast( + UGameplayStatics::CreateSaveGameObject(UZMMOUISaveGame::StaticClass())); +} + +void UZMMOUISaveGame::SaveNow(UZMMOUISaveGame* Save) +{ + if (Save) + { + UGameplayStatics::SaveGameToSlot(Save, SlotName, UserIndex); + } +} + +bool UZMMOUISaveGame::TryGetWindowPosition(FName WindowId, FVector2D& OutPos) const +{ + if (const FVector2D* Found = WindowPositions.Find(WindowId)) + { + OutPos = *Found; + return true; + } + OutPos = FVector2D::ZeroVector; + return false; +} + +void UZMMOUISaveGame::SetWindowPosition(FName WindowId, FVector2D Pos) +{ + WindowPositions.Add(WindowId, Pos); +} diff --git a/Source/ZMMO/Game/UI/Common/UIWindowSaveGame.h b/Source/ZMMO/Game/UI/Common/UIWindowSaveGame.h new file mode 100644 index 0000000..76e6f35 --- /dev/null +++ b/Source/ZMMO/Game/UI/Common/UIWindowSaveGame.h @@ -0,0 +1,46 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/SaveGame.h" +#include "UIWindowSaveGame.generated.h" + +/** + * Persistencia de prefs de UI por janela (posicao, e futuramente tamanho, + * estado expandido, etc). Usado por UUIDraggableWindow_Base — toda janela + * movel registra uma chave (FName WindowId) e grava aqui ao soltar o drag. + * + * Arquivo: [Project]/Saved/SaveGames/UIPrefs_0.sav (binary blob via + * UE serializer). Mesmo padrao do [[ZMMOLoginSaveGame]]. + * + * Helpers static abaixo encapsulam load-or-create do slot e Get/Set/Save + * pra que a Base nao precise repetir a cerimonia. + */ +UCLASS() +class ZMMO_API UZMMOUISaveGame : public USaveGame +{ + GENERATED_BODY() + +public: + /** Posicao normalizada (translation absoluta em pixels da viewport) por janela. */ + UPROPERTY() + TMap WindowPositions; + + static const FString SlotName; + static constexpr int32 UserIndex = 0; + + /** + * Carrega o save existente ou cria um novo (nao salva ainda). + * Sempre retorna instancia valida. + */ + static UZMMOUISaveGame* LoadOrCreate(); + + /** Salva o slot atual (sincrono — chamado on-mouse-up, custo baixo). */ + static void SaveNow(UZMMOUISaveGame* Save); + + /** Get com default — se WindowId nao foi gravado retorna FVector2D::ZeroVector. */ + UFUNCTION(BlueprintCallable, Category = "UI|Prefs") + bool TryGetWindowPosition(FName WindowId, FVector2D& OutPos) const; + + UFUNCTION(BlueprintCallable, Category = "UI|Prefs") + void SetWindowPosition(FName WindowId, FVector2D Pos); +}; diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp index 31ba12b..5aaf239 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp +++ b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp @@ -233,6 +233,16 @@ void UUIFrontEndFlowSubsystem::ResolveAndPushScreen(EZMMOFrontEndState State) GetLoadingProfiles(), GetLoadingTips()); Loading->OnLoadingComplete.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleLoadingComplete); Loading->MarkStepDone(TEXT("Travel")); + + // Drena steps que dispararam ANTES do widget existir + // (HandlePostLoadMap/HandlePlayerSpawned podem ter + // rodado durante o RequestAsyncLoad). Sem isso o + // loading fica eterno se o servidor for rápido demais. + for (const FName StepId : PendingLoadingSteps_) + { + Loading->MarkStepDone(StepId); + } + PendingLoadingSteps_.Reset(); } } } @@ -423,8 +433,10 @@ void UUIFrontEndFlowSubsystem::HandlePostLoadMap(UWorld* /*LoadedWorld*/) return; } - bTravelingToWorld = false; - SetState(EZMMOFrontEndState::InWorld); + // Race fix: widget ainda em RequestAsyncLoad; memoiza pra drenar quando + // a tela for criada (ver lambda em ResolveAndPushScreen). Não fallback + // pra InWorld — esperamos a tela existir e o Spawn chegar. + PendingLoadingSteps_.Add(TEXT("MapLoaded")); } void UUIFrontEndFlowSubsystem::HandlePlayerSpawned(int32 /*EntityId*/, bool bIsLocal, @@ -435,6 +447,15 @@ void UUIFrontEndFlowSubsystem::HandlePlayerSpawned(int32 /*EntityId*/, bool bIsL if (UUILoadingScreen_Base* Loading = ActiveLoadingScreen.Get()) { Loading->MarkStepDone(TEXT("Spawn")); + return; + } + + // Race fix: spawn pode chegar ANTES do widget existir se o servidor for + // rápido (TryFinalizeEnterWorld) e o RequestAsyncLoad lento. Memoiza + // pra drenar no callback do async load. + if (bTravelingToWorld) + { + PendingLoadingSteps_.Add(TEXT("Spawn")); } } @@ -445,6 +466,7 @@ void UUIFrontEndFlowSubsystem::HandleLoadingComplete() Loading->OnLoadingComplete.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleLoadingComplete); } ActiveLoadingScreen.Reset(); + PendingLoadingSteps_.Reset(); bTravelingToWorld = false; SetState(EZMMOFrontEndState::InWorld); } diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h index d1168de..18339f6 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h +++ b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h @@ -249,6 +249,15 @@ private: */ TWeakObjectPtr ActiveLoadingScreen; + /** + * Race fix: o widget de loading é criado via RequestAsyncLoad (assíncrono). + * Se o servidor for rápido (TryFinalizeEnterWorld), HandlePostLoadMap e/ou + * HandlePlayerSpawned podem disparar ANTES de ActiveLoadingScreen existir + * — nesse caso o MarkStepDone vira no-op e o loading fica eterno. + * Memoizamos os StepIds aqui e drenamos quando a tela for criada. + */ + TSet PendingLoadingSteps_; + bool bNetBound = false; bool bTravelingToWorld = false; FDelegateHandle PostLoadMapHandle; diff --git a/Source/ZMMO/Game/UI/InGame/UIInGameFlowSubsystem.cpp b/Source/ZMMO/Game/UI/InGame/UIInGameFlowSubsystem.cpp index cb16f33..bd4a318 100644 --- a/Source/ZMMO/Game/UI/InGame/UIInGameFlowSubsystem.cpp +++ b/Source/ZMMO/Game/UI/InGame/UIInGameFlowSubsystem.cpp @@ -81,16 +81,26 @@ void UUIInGameFlowSubsystem::SetState(EZMMOInGameUIState NewState) // (Status/Inventory/Menu) limpando o Layer.GameMenu — HUD em Layer.Game // permanece. Para qualquer outro estado, mantemos HUD e empilhamos no // layer correspondente. + const bool bGoingToPlaying = (NewState == EZMMOInGameUIState::Playing); if (UUIManagerSubsystem* Mgr = GetUIManager()) { - const bool bGoingToPlaying = (NewState == EZMMOInGameUIState::Playing); if (bGoingToPlaying) { Mgr->ClearLayer(ZMMOUITags::UI_Layer_GameMenu.GetTag()); } } - ResolveAndPushScreen(NewState); + // Skip re-push do HUD quando ja' estavamos in-game (Old != None significa + // que StartInGame ja' rodou e o HUD esta no Layer.Game). Senao, cada vez + // que o usuario fechar StatusWindow/Inventory/etc, o HUD seria recriado + // — flicker em todos os textos (HP/SP/etc) por causa do Construct novo. + const bool bSkipRepush = bGoingToPlaying + && OldState != EZMMOInGameUIState::None + && OldState != EZMMOInGameUIState::Playing; + if (!bSkipRepush) + { + ResolveAndPushScreen(NewState); + } OnStateChanged.Broadcast(NewState); } diff --git a/Source/ZMMO/Game/UI/Widgets/UIPlayerStatus_Window.cpp b/Source/ZMMO/Game/UI/Widgets/UIPlayerStatus_Window.cpp index dca2771..5a6bad1 100644 --- a/Source/ZMMO/Game/UI/Widgets/UIPlayerStatus_Window.cpp +++ b/Source/ZMMO/Game/UI/Widgets/UIPlayerStatus_Window.cpp @@ -2,7 +2,9 @@ #include "CommonInputTypeEnum.h" #include "Components/Button.h" +#include "Components/HorizontalBox.h" #include "Components/TextBlock.h" +#include "InputCoreTypes.h" #include "Engine/World.h" #include "GameFramework/PlayerController.h" #include "GameFramework/PlayerState.h" @@ -20,8 +22,18 @@ UUIPlayerStatus_Window::UUIPlayerStatus_Window(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { - bIsBackHandler = true; + // bIsBackHandler exige DefaultBackAction configurada no CommonUISettings; + // projeto ainda não tem DT de input actions, então registrar o handler + // poluía o log com "Cannot create action binding". Fechamento da janela + // vai por Alt+A (NativeOnKeyDown) e Button_Close. bAutoActivate = true; + // Focusable + DesiredFocusTarget=self → recebe NativeOnKeyDown (Alt+A). + SetIsFocusable(true); +} + +UWidget* UUIPlayerStatus_Window::NativeGetDesiredFocusTarget() const +{ + return const_cast(this); } TOptional UUIPlayerStatus_Window::GetDesiredInputConfig() const @@ -51,7 +63,14 @@ void UUIPlayerStatus_Window::NativeOnActivated() if (TitleText) TitleText->SetText(FText::FromString(TEXT("Status"))); - if (CloseBtn) CloseBtn->OnClicked.AddDynamic(this, &UUIPlayerStatus_Window::OnCloseClicked); + if (Button_Close) Button_Close->OnClicked.AddDynamic(this, &UUIPlayerStatus_Window::HandleCloseClicked); + + // Background_Panel default é SELF_HIT_TEST_INVISIBLE; precisa Visible + // pra capturar clicks em areas vazias do modal e bubbla pra Window. + if (Background_Panel) + { + Background_Panel->SetVisibility(ESlateVisibility::Visible); + } BindToLocalPlayer(); @@ -69,7 +88,7 @@ void UUIPlayerStatus_Window::NativeOnDeactivated() UnbindRow(Row_Str); UnbindRow(Row_Agi); UnbindRow(Row_Vit); UnbindRow(Row_Int); UnbindRow(Row_Dex); UnbindRow(Row_Luk); - if (CloseBtn) CloseBtn->OnClicked.RemoveDynamic(this, &UUIPlayerStatus_Window::OnCloseClicked); + if (Button_Close) Button_Close->OnClicked.RemoveDynamic(this, &UUIPlayerStatus_Window::HandleCloseClicked); Super::NativeOnDeactivated(); } @@ -219,7 +238,7 @@ void UUIPlayerStatus_Window::RefreshFromSnapshot(const FZMMOAttributesSnapshot& SetAllPrimaryRowsPlusEnabled(bCanAlloc); } -void UUIPlayerStatus_Window::OnCloseClicked() +void UUIPlayerStatus_Window::HandleCloseClicked() { if (UGameInstance* GI = GetGameInstance()) { @@ -229,3 +248,33 @@ void UUIPlayerStatus_Window::OnCloseClicked() } } } + +FReply UUIPlayerStatus_Window::NativeOnKeyDown(const FGeometry& Geom, const FKeyEvent& KeyEvent) +{ + // Alt+A em modo Menu — PlayerController nao recebe (CommonUI absorve). + // Handle aqui pra completar o toggle. + if (KeyEvent.GetKey() == EKeys::A && KeyEvent.IsAltDown()) + { + if (UGameInstance* GI = GetGameInstance()) + { + if (UUIInGameFlowSubsystem* Flow = GI->GetSubsystem()) + { + Flow->SetState(EZMMOInGameUIState::Playing); + return FReply::Handled(); + } + } + } + return Super::NativeOnKeyDown(Geom, KeyEvent); +} + +bool UUIPlayerStatus_Window::IsDragHandleHit(const FPointerEvent& MouseEvent) const +{ + // Drag em qualquer parte do modal EXCETO Container_Stats (rows + botões +STR). + // Botão close consome o evento antes (UButton.OnClicked) e não bubbla aqui. + const FVector2D Pos = MouseEvent.GetScreenSpacePosition(); + if (Container_Stats && Container_Stats->GetCachedGeometry().IsUnderLocation(Pos)) + { + return false; + } + return true; +} diff --git a/Source/ZMMO/Game/UI/Widgets/UIPlayerStatus_Window.h b/Source/ZMMO/Game/UI/Widgets/UIPlayerStatus_Window.h index 6440ed9..b1541ff 100644 --- a/Source/ZMMO/Game/UI/Widgets/UIPlayerStatus_Window.h +++ b/Source/ZMMO/Game/UI/Widgets/UIPlayerStatus_Window.h @@ -1,12 +1,13 @@ #pragma once #include "CoreMinimal.h" -#include "CommonActivatableWidget.h" +#include "UI/Common/UIDraggableWindow_Base.h" #include "ZMMOAttributeTypes.h" #include "UIPlayerStatus_Window.generated.h" class UButton; class UTextBlock; +class UHorizontalBox; class UUIPlayerStatus_BonusRow; class UUIPlayerStatus_StatRow; class UZMMOAttributeComponent; @@ -22,18 +23,46 @@ class UZMMOAttributeComponent; * pra suportar variantes (ex.: HUD compacto sem CRIT/ASPD). */ UCLASS(Abstract, Blueprintable, BlueprintType) -class ZMMO_API UUIPlayerStatus_Window : public UCommonActivatableWidget +class ZMMO_API UUIPlayerStatus_Window : public UUIDraggableWindow_Base { GENERATED_BODY() public: UUIPlayerStatus_Window(const FObjectInitializer& ObjectInitializer); + /** Chave de persistencia no UZMMOUISaveGame. Estavel — nao renomear. */ + virtual FName GetWindowId() const override { return TEXT("PlayerStatus"); } + protected: virtual void NativeOnActivated() override; virtual void NativeOnDeactivated() override; virtual TOptional GetDesiredInputConfig() const override; + /** Diz ao stack pra mover foco pra ca' (precisa pra NativeOnKeyDown ser chamado). */ + virtual UWidget* NativeGetDesiredFocusTarget() const override; + + /** Atalho Alt+A — fecha a janela em modo Menu (PC controller nao recebe). */ + virtual FReply NativeOnKeyDown(const FGeometry& Geom, const FKeyEvent& KeyEvent) override; + + /** Drag area = Container_Header (header inteiro). Header_DragHandle no WBP nao necessario. */ + virtual bool IsDragHandleHit(const FPointerEvent& MouseEvent) const override; + + /** Header do WBP (HorizontalBox raiz da identidade) — usado como drag area. */ + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Header") TObjectPtr Container_Header; + + /** + * Zona EXCLUÍDA do drag — é onde ficam os rows interativos (+STR etc.). + * Clicar aqui não deve iniciar drag mesmo em area "vazia" entre rows. + */ + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr Container_Stats; + + /** + * Panel de fundo (UIPanel_Base). Setado pra Visible em NativeOnActivated + * pra capturar clicks em areas vazias do modal — sem isso, drag só funciona + * onde algum filho é Visible (header/footer). + */ + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr Background_Panel; + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr TitleText; UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr StatusPointText; @@ -59,7 +88,8 @@ protected: UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") TObjectPtr Row_Crit; UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status|Derived") TObjectPtr Row_Aspd; - UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr CloseBtn; + /** Botao de fechar na header — UButton simples (placeholder, estilizar depois). */ + UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional), Category = "Zeus|Status") TObjectPtr Button_Close; private: void BindToLocalPlayer(); @@ -71,7 +101,7 @@ private: UFUNCTION() void HandleAttributesChanged(const FZMMOAttributesSnapshot& Snapshot); UFUNCTION() void HandleStatAllocReply(bool bAccepted, int32 Reason); UFUNCTION() void HandleRowPlusClicked(int32 StatId); - UFUNCTION() void OnCloseClicked(); + UFUNCTION() void HandleCloseClicked(); UPROPERTY(Transient) TWeakObjectPtr BoundComponent;