diff --git a/Content/Widgets/WB_Crosshair.uasset b/Content/Widgets/WB_Crosshair.uasset index 79fe7682..c649a4dc 100644 --- a/Content/Widgets/WB_Crosshair.uasset +++ b/Content/Widgets/WB_Crosshair.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c281245cf21c89bfd5077e6ff1365fa6439a00c21903ca9192b8f35657de859b -size 59045 +oid sha256:43ed7d9eeab2b723ad3564402467d8781220121b23ce3fc4cfa978a9a3edef52 +size 52875 diff --git a/Source/Integration/PlayerControllerBase.cpp b/Source/Integration/PlayerControllerBase.cpp index e865b20a..f32553ce 100644 --- a/Source/Integration/PlayerControllerBase.cpp +++ b/Source/Integration/PlayerControllerBase.cpp @@ -163,8 +163,7 @@ void AlxPlayerControllerBase::AddWidgetToRoot(UUserWidget *Widget) if (Widget->GetParent() == PC->RootCanvas) return; - UlxRootCanvasSlot *Slot = PC->RootCanvas->AddChildToRootCanvas(Widget); - Slot->SlotUnderConstruction = false; + PC->RootCanvas->AddChildToRootCanvas(Widget); } void AlxPlayerControllerBase::BuildInputStack(TArray& InputStack) @@ -239,7 +238,7 @@ void AlxPlayerControllerBase::BuildInputStack(TArray& InputSta } // Now add the widget-managed input components. - for (UlxRootCanvasSlot *Slot : ReverseIterate(WidgetSlots)) + for (UlxRootCanvasSlot *Slot : WidgetSlots) { if (Slot->EnableEnhancedInput) { @@ -269,15 +268,14 @@ void AlxPlayerControllerBase::UpdateInputMode() if (!SlateUser.IsValid()) return; - // Get the desired configuration from the first widget. - // TODO: Maybe we don't have to sort the whole array. - TArray WidgetSlots = RootCanvas->GetSortedUserWidgets(); + RootCanvas->UpdateZOrders(); + + // Get the desired configuration from the top widget. UUserWidget *Widget = nullptr; UWidget *Focus = nullptr; bool ShowPointer = false; - if (!WidgetSlots.IsEmpty()) + if (UlxRootCanvasSlot *Top = RootCanvas->GetTopWidget()) { - UlxRootCanvasSlot *Top = WidgetSlots[0]; Widget = Cast(Top->GetContent()); Focus = Widget->GetDesiredFocusWidget(); ShowPointer = Top->ShowPointer; diff --git a/Source/Integration/RootCanvas.cpp b/Source/Integration/RootCanvas.cpp index 9fd1cb5b..1849de4c 100644 --- a/Source/Integration/RootCanvas.cpp +++ b/Source/Integration/RootCanvas.cpp @@ -18,33 +18,6 @@ UlxRootCanvasSlot::UlxRootCanvasSlot(const FObjectInitializer& ObjectInitializer SetOffsets(FMargin(0.0f, 0.0f, 0.0f, 0.0f)); } -void UlxRootCanvasSlot::SetZOrderReliable(int32 Order) -{ - if (SlotUnderConstruction) - { - PRAGMA_DISABLE_DEPRECATION_WARNINGS - ZOrder = Order; - PRAGMA_ENABLE_DEPRECATION_WARNINGS - } - else - { - SetZOrder(Order); - } -} - -int32 UlxRootCanvasSlot::GetZOrderReliable() -{ - if (SlotUnderConstruction) - { - PRAGMA_DISABLE_DEPRECATION_WARNINGS - return ZOrder; - PRAGMA_ENABLE_DEPRECATION_WARNINGS - } - else - { - return GetZOrder(); - } -} UClass* UlxRootCanvasPanel::GetSlotClass() const { @@ -70,16 +43,24 @@ UlxRootCanvasSlot* UlxRootCanvasPanel::AddChildToRootCanvas(UWidget* Content) return Cast(Super::AddChild(Content)); } -int32 UlxRootCanvasPanel::GetMaxZOrder() const +void UlxRootCanvasPanel::OnSlotAdded(UPanelSlot* InSlot) { - int32 MaxZOrder = 0; - for (UPanelSlot *PanelSlot : Slots) + UlxRootCanvasSlot *Slot = CastChecked(InSlot); + Slot->BringToFrontCount = ++BringToFrontCounter; + Super::OnSlotAdded(InSlot); +} + + +UlxRootCanvasSlot* UlxRootCanvasPanel::GetTopWidget() +{ + UlxRootCanvasSlot* Top = nullptr; + for (UPanelSlot* PanelSlot : Slots) { - UlxRootCanvasSlot *TypedSlot = Cast(PanelSlot); - check(TypedSlot); - MaxZOrder = FMath::Max(MaxZOrder, TypedSlot->GetZOrderReliable()); + UlxRootCanvasSlot* Slot = CastChecked(PanelSlot); + if (Cast(Slot->Content) == nullptr) continue; + if ((Top == nullptr) || (*Top < *Slot)) Top = Slot; } - return MaxZOrder; + return Top; } TArray UlxRootCanvasPanel::GetSortedUserWidgets() @@ -95,12 +76,33 @@ TArray UlxRootCanvasPanel::GetSortedUserWidgets() } Result.StableSort([](const UlxRootCanvasSlot &A, const UlxRootCanvasSlot &B) { - return A.GetZOrder() > B.GetZOrder(); + return A < B; }); return Result; } +void UlxRootCanvasPanel::UpdateZOrders() +{ + for (UPanelSlot *PanelSlot : Slots) + { + UlxRootCanvasSlot *Slot = Cast(PanelSlot); + check(Slot); + int32 ZOrder = (Slot->ShowPointer ? 1000000 : 0) + Slot->BringToFrontCount; + Slot->SetZOrder(ZOrder); + } +} + +void UlxRootCanvasPanel::BringToFront(UUserWidget *Widget) +{ + if (!IsValid(Widget)) return; + UlxRootCanvasSlot *Slot = Cast(Widget->Slot); + if (!Slot) return; + UlxRootCanvasPanel *Panel = Cast(Slot->Parent); + if (!Panel) return; + Slot->BringToFrontCount = ++Panel->BringToFrontCounter; +} + void UlxRootCanvasPanel::SetWidgetWindowManagement(class UUserWidget *Widget, bool ShowPointer, bool BlockInput, bool EnableEnhancedInput, bool BringToFront, UWidget *DesiredFocusWidget) { @@ -120,7 +122,15 @@ void UlxRootCanvasPanel::SetWidgetWindowManagement(class UUserWidget *Widget, Slot->ShowPointer = ShowPointer; Slot->BlockInput = BlockInput; Slot->EnableEnhancedInput = EnableEnhancedInput; - Widget->SetDesiredFocusWidget(DesiredFocusWidget); - if (BringToFront) Slot->SetZOrderReliable(Panel->GetMaxZOrder() + 1); + if (DesiredFocusWidget) + { + if (!Widget->SetDesiredFocusWidget(DesiredFocusWidget)) + UE_LOG(LogLuprexIntegration, Error, TEXT("SetWidgetWindowManagement: focus widget must be a child of widget")); + } + else + { + Widget->SetDesiredFocusWidget(NAME_None); + } + if (BringToFront) Slot->BringToFrontCount = ++Panel->BringToFrontCounter; } diff --git a/Source/Integration/RootCanvas.h b/Source/Integration/RootCanvas.h index 6953782a..a529aa7a 100644 --- a/Source/Integration/RootCanvas.h +++ b/Source/Integration/RootCanvas.h @@ -2,13 +2,16 @@ // // RootCanvas.h // -// UlxRootCanvasPanel is a UCanvasPanel subclass whose -// slots (UlxRootCanvasSlot) carry input-mode configuration -// in addition to layout. The PlayerController scans these -// slots, sorted by ZOrder, to arbitrate pointer visibility, -// capture, focus, and input-component blocking for -// top-level widgets. ZOrder therefore serves double duty: -// it determines draw order AND input priority. +// Luprex provides a "window management system" for root +// widgets. In this system, all top-level widgets have to go +// into the root canvas (instead of the viewport). The +// window management system monitors the widgets within the +// root canvas and continuously updates the z-orders, the +// pointer visibility, the mouse capture mode, the keyboard +// focus, and enhanced input event routing based on hints +// and directives given by the widgets. +// +// To learn more, read Docs/Luprex-Window-Management.md // //////////////////////////////////////////////////////////// @@ -29,11 +32,6 @@ class UWidget; // // UlxRootCanvasSlot // -// Luprex provides a "window management system" for root widgets. -// This system is documented in Docs/Keyboard-Focus-and-Input-Modes.md -// The Root Canvas Slot is how widgets ask the window management system -// to engage certain behaviors. -// //////////////////////////////////////////////////////////// UCLASS() class INTEGRATION_API UlxRootCanvasSlot : public UCanvasPanelSlot @@ -60,32 +58,24 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Luprex|Input Mode") bool EnableEnhancedInput = true; - // Knowing whether the slot is under construction helps us to work - // around some bugs in Unreal. See below. - bool SlotUnderConstruction = true; - - // Reliable version of SetZOrder. There is a bug in the normal version of - // SetZOrder: it crashes if you use it during OnConstruct. We have a - // workaround, but it requires using a different getter and setter. - UFUNCTION(BlueprintCallable, Category = "Luprex|Input Mode") - void SetZOrderReliable(int32 Order); + // Sequence number assigned when this slot was last brought to front. + int32 BringToFrontCount = 0; - // Reliable version of GetZOrder. There is a bug in the normal version of - // SetZOrder: it crashes if you use it during OnConstruct. We have a - // workaround, but it requires using a different getter and setter. - UFUNCTION(BlueprintCallable, Category = "Luprex|Input Mode") - int32 GetZOrderReliable(); + // Widget Z Ordering: widgets that show the pointer go on top of windows + // that don't, and within a group, widgets that have greater BringToFrontCount + // go on top. Sorting an array with this puts the top windows at the end + // of the array. + bool operator<(const UlxRootCanvasSlot &Other) const + { + if (ShowPointer != Other.ShowPointer) return !ShowPointer; + return BringToFrontCount < Other.BringToFrontCount; + } }; //////////////////////////////////////////////////////////// // // UlxRootCanvasPanel // -// A UCanvasPanel that uses UlxRootCanvasSlot for its -// children instead of the plain UCanvasPanelSlot. Layout -// behavior is identical to UCanvasPanel; only the slot -// type differs. -// //////////////////////////////////////////////////////////// UCLASS() class INTEGRATION_API UlxRootCanvasPanel : public UCanvasPanel @@ -94,18 +84,18 @@ class INTEGRATION_API UlxRootCanvasPanel : public UCanvasPanel public: - // Convenience wrapper around AddChild that returns the - // derived slot type, so callers don't have to cast. + // Add a child to the canvas. Also brings the child to the front + // for the first time, ensuring that every widget has a unique + // BringToFront counter. UFUNCTION() UlxRootCanvasSlot* AddChildToRootCanvas(UWidget* Content); // Find children of type UserWidget. Return them in a sorted - // order, with the highest Zorder first. + // order, with the bottom window first and the top window last. TArray GetSortedUserWidgets(); - // Return the largest ZOrder across all slots, or 0 if empty. - // Used as the basis for placing new widgets on top. - int32 GetMaxZOrder() const; + // Return the highest-priority UserWidget slot, or nullptr if there are none. + UlxRootCanvasSlot* GetTopWidget(); // This function updates several window-management-related properties // which are stored in the UserWidget and the lxRootCanvasSlot. Note that @@ -119,6 +109,17 @@ public: bool ShowPointer, bool BlockInput, bool EnableEnhancedInput, bool BringToFront, UWidget *DesiredFocusWidget); + // Bring the widget to the front of the Z order within its group. + // This is implemented using the BringToFront counter. + UFUNCTION(BlueprintCallable, Category = "Luprex|Window Management", + meta = (DefaultToSelf = "Widget")) + static void BringToFront(class UUserWidget *Widget); + + // Recompute and apply ZOrder values for all slots based on ShowPointer and + // BringToFrontCount. Must be called from a context where SetZOrder is safe - + // i.e. not during OnConstruct. + void UpdateZOrders(); + // Fetch the UlxRootCanvasSlot for a widget that is parented to a // UlxRootCanvasPanel. Returns nullptr via the WrongType exec pin // if the widget isn't a root widget (no slot, or slot is not a @@ -129,8 +130,17 @@ public: protected: - // UPanelWidget + // We inherit most of our code from CanvasPanel. This causes the + // CanvasPanel code to allocate slots of type UlxRootCanvasSlot. virtual UClass* GetSlotClass() const override; + + // We override OnSlotAdded in order to be able to be able to + // fully finish initializing the slot before the widget + // OnConstruct method has a chance to execute. + virtual void OnSlotAdded(UPanelSlot* InSlot) override; + + // Monotonic counter incremented each time any slot is brought to front. + int32 BringToFrontCounter = 0; }; diff --git a/tools/UEDataFormatter.py b/tools/UEDataFormatter.py index 2e1dfcb6..eb42edc6 100644 --- a/tools/UEDataFormatter.py +++ b/tools/UEDataFormatter.py @@ -16,26 +16,32 @@ _FUObjectItemType = None _GNameBlocksDebug = None _GObjectArrayForDebugVisualizers = None -def _init_globals(target): +def _init_globals(frame, bp_loc, dict): global _FNameEntryType, _FNameEntryStride, _FUObjectItemType global _GNameBlocksDebug, _GObjectArrayForDebugVisualizers - _FNameEntryType = target.FindFirstType('FNameEntry') - if not _FNameEntryType.IsValid(): - print('UEDataFormatter: FNameEntry type not found') - _FNameEntryStride = _FNameEntryType.GetByteAlign() + target = frame.GetThread().GetProcess().GetTarget() - _FUObjectItemType = target.FindFirstType('FUObjectItem') - if not _FUObjectItemType.IsValid(): - print('UEDataFormatter: FUObjectItem type not found') + if _FNameEntryType is None: + t = target.FindFirstType('FNameEntry') + if t.IsValid(): + _FNameEntryType = t + _FNameEntryStride = t.GetByteAlign() - _GNameBlocksDebug = target.FindFirstGlobalVariable('GNameBlocksDebug') - if not _GNameBlocksDebug.IsValid(): - print('UEDataFormatter: GNameBlocksDebug not found') - - _GObjectArrayForDebugVisualizers = target.FindFirstGlobalVariable('GObjectArrayForDebugVisualizers') - if not _GObjectArrayForDebugVisualizers.IsValid(): - print('UEDataFormatter: GObjectArrayForDebugVisualizers not found') + if _FUObjectItemType is None: + t = target.FindFirstType('FUObjectItem') + if t.IsValid(): + _FUObjectItemType = t + + if _GNameBlocksDebug is None: + v = target.FindFirstGlobalVariable('GNameBlocksDebug') + if v.IsValid(): + _GNameBlocksDebug = v + + if _GObjectArrayForDebugVisualizers is None: + v = target.FindFirstGlobalVariable('GObjectArrayForDebugVisualizers') + if v.IsValid(): + _GObjectArrayForDebugVisualizers = v ############################################################ @@ -677,8 +683,11 @@ def _register_provider(cat, pattern, summary_fn=None, synth_cls=None): def __lldb_init_module(debugger, dict): print("Running lldb_init_module") - _init_globals(debugger.GetSelectedTarget()) + debugger.HandleCommand('target stop-hook add --python-function ' + __name__ + '._init_globals') debugger.HandleCommand('type category delete ' + __name__) + frame = debugger.GetSelectedTarget().GetProcess().GetSelectedThread().GetSelectedFrame() + if frame.IsValid(): + _init_globals(frame, None, {}) cat = debugger.CreateCategory(__name__) _register_provider(cat, '^FString$', summary_fn='UEFStringSummaryProvider')