174 lines
6.9 KiB
C++
174 lines
6.9 KiB
C++
|
|
#pragma once
|
||
|
|
|
||
|
|
#include <vector>
|
||
|
|
|
||
|
|
#include "Widget.hpp"
|
||
|
|
#include "PanelSlot.hpp"
|
||
|
|
|
||
|
|
namespace PalUIExtension::Api
|
||
|
|
{
|
||
|
|
class PanelWidget : public Widget
|
||
|
|
{
|
||
|
|
public:
|
||
|
|
static auto ClassPath() -> const TCHAR* { return STR("/Script/UMG.PanelWidget"); }
|
||
|
|
static auto DebugTag() -> const TCHAR* { return STR("PanelWidget"); }
|
||
|
|
|
||
|
|
PanelWidget() = default;
|
||
|
|
explicit PanelWidget(UObject* Obj)
|
||
|
|
: Widget(Obj, ClassOf<PanelWidget>(), DebugTag()) {}
|
||
|
|
|
||
|
|
auto GetChildrenCount() -> std::optional<int32>
|
||
|
|
{
|
||
|
|
return CallForReturn<int32>(STR("GetChildrenCount"));
|
||
|
|
}
|
||
|
|
auto HasAnyChildren() -> std::optional<bool>
|
||
|
|
{
|
||
|
|
return CallForReturn<bool>(STR("HasAnyChildren"));
|
||
|
|
}
|
||
|
|
|
||
|
|
auto GetChildAt(int32 Index) -> Widget
|
||
|
|
{
|
||
|
|
struct P { int32 Index; UObject* ReturnValue; } p{ Index, nullptr };
|
||
|
|
auto Result = CallForReturn(STR("GetChildAt"), p, &P::ReturnValue);
|
||
|
|
return (Result && *Result) ? Widget{ *Result } : Widget{};
|
||
|
|
}
|
||
|
|
|
||
|
|
auto GetChildIndex(UObject* Content) -> std::optional<int32>
|
||
|
|
{
|
||
|
|
struct P { UObject* Content; int32 ReturnValue; } p{ Content, -1 };
|
||
|
|
return CallForReturn(STR("GetChildIndex"), p, &P::ReturnValue);
|
||
|
|
}
|
||
|
|
|
||
|
|
auto HasChild(UObject* Content) -> std::optional<bool>
|
||
|
|
{
|
||
|
|
struct P { UObject* Content; bool ReturnValue; } p{ Content, false };
|
||
|
|
return CallForReturn(STR("HasChild"), p, &P::ReturnValue);
|
||
|
|
}
|
||
|
|
|
||
|
|
auto AddChild(UObject* Content) -> PanelSlot
|
||
|
|
{
|
||
|
|
struct P { UObject* Content; UObject* ReturnValue; } p{ Content, nullptr };
|
||
|
|
auto Result = CallForReturn(STR("AddChild"), p, &P::ReturnValue);
|
||
|
|
return (Result && *Result) ? PanelSlot{ *Result } : PanelSlot{};
|
||
|
|
}
|
||
|
|
|
||
|
|
auto RemoveChild(UObject* Content) -> std::optional<bool>
|
||
|
|
{
|
||
|
|
struct P { UObject* Content; bool ReturnValue; } p{ Content, false };
|
||
|
|
return CallForReturn(STR("RemoveChild"), p, &P::ReturnValue);
|
||
|
|
}
|
||
|
|
|
||
|
|
auto RemoveChildAt(int32 Index) -> std::optional<bool>
|
||
|
|
{
|
||
|
|
struct P { int32 Index; bool ReturnValue; } p{ Index, false };
|
||
|
|
return CallForReturn(STR("RemoveChildAt"), p, &P::ReturnValue);
|
||
|
|
}
|
||
|
|
|
||
|
|
auto ClearChildren() -> bool { return CallVoid(STR("ClearChildren")); }
|
||
|
|
|
||
|
|
// ------------------------------------------------------------------
|
||
|
|
// Deliberately NOT a wrapper around the engine's GetAllChildren.
|
||
|
|
//
|
||
|
|
// That UFUNCTION returns TArray<UWidget*> by value. ProcessEvent
|
||
|
|
// constructs that array into the frame with the engine's allocator
|
||
|
|
// and then hands you ownership of the allocation. Getting that
|
||
|
|
// wrong is either a leak on every call or a free across a module
|
||
|
|
// boundary, and neither shows up as a crash near the cause.
|
||
|
|
//
|
||
|
|
// The count + index loop is two extra ProcessEvent calls per child
|
||
|
|
// and zero ownership questions. If you are calling this per-frame
|
||
|
|
// over a large panel, cache the result; if you are calling it once
|
||
|
|
// in a Construct hook, the cost is irrelevant.
|
||
|
|
// ------------------------------------------------------------------
|
||
|
|
auto GetAllChildren() -> std::vector<Widget>
|
||
|
|
{
|
||
|
|
std::vector<Widget> Children;
|
||
|
|
|
||
|
|
auto Count = GetChildrenCount();
|
||
|
|
if (!Count || *Count <= 0) return Children;
|
||
|
|
|
||
|
|
Children.reserve(static_cast<size_t>(*Count));
|
||
|
|
for (int32 i = 0; i < *Count; ++i)
|
||
|
|
{
|
||
|
|
Widget Child = GetChildAt(i);
|
||
|
|
if (Child) Children.push_back(Child);
|
||
|
|
}
|
||
|
|
return Children;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ------------------------------------------------------------------
|
||
|
|
// Children WITHOUT ProcessEvent.
|
||
|
|
//
|
||
|
|
// GetAllChildren() above costs 1 + N ProcessEvent calls. That is
|
||
|
|
// fine once in a Construct hook and not fine as the inner loop of a
|
||
|
|
// whole-tree walk, where it becomes 1 + N per PANEL.
|
||
|
|
//
|
||
|
|
// UPanelWidget::Slots is a TArray<UPanelSlot*> UPROPERTY (confirmed
|
||
|
|
// in the dump: UHTHeaderDump/UMG/Public/PanelWidget.h line 14), and
|
||
|
|
// UPanelSlot::Content is a UPROPERTY too. So the full child list is
|
||
|
|
// reachable as pure memory reads. Slot order is child order -- the
|
||
|
|
// engine's own GetChildAt(i) is Slots[i]->Content.
|
||
|
|
//
|
||
|
|
// Nothing here is copied out of engine memory except the pointers
|
||
|
|
// themselves, which is what makes reading the TArray safe; see the
|
||
|
|
// TArrayRead_ comment in UmgTypes.hpp for where that line is.
|
||
|
|
//
|
||
|
|
// Returns false if Slots could not be read or looked implausible,
|
||
|
|
// WITHOUT touching Out -- so a caller can fall back to
|
||
|
|
// GetAllChildren() and know it isn't double-appending.
|
||
|
|
// ------------------------------------------------------------------
|
||
|
|
static constexpr int32 kMaxPlausibleChildren = 4096;
|
||
|
|
|
||
|
|
auto TryGetChildObjects(std::vector<UObject*>& Out) const -> bool
|
||
|
|
{
|
||
|
|
auto* Slots = TryPropertyPtr<TArrayRead_<UObject*>>(STR("Slots"));
|
||
|
|
if (!Slots) return false;
|
||
|
|
|
||
|
|
if (!Slots->IsPlausible(kMaxPlausibleChildren))
|
||
|
|
{
|
||
|
|
Output::send<LogLevel::Warning>(
|
||
|
|
STR("[{}] 'Slots' does not look like a live TArray ")
|
||
|
|
STR("(Num={}, Max={}, Data={}) -- falling back to ProcessEvent\n"),
|
||
|
|
m_tag, Slots->Num, Slots->Max,
|
||
|
|
Slots->Data ? STR("set") : STR("null"));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
Out.reserve(Out.size() + static_cast<size_t>(Slots->Num));
|
||
|
|
for (int32 i = 0; i < Slots->Num; ++i)
|
||
|
|
{
|
||
|
|
UObject* Slot = Slots->Data[i];
|
||
|
|
if (!Slot) continue;
|
||
|
|
|
||
|
|
// Read Content off the slot directly rather than through the
|
||
|
|
// PanelSlot wrapper: the wrapper's IsA check would log on
|
||
|
|
// anything unexpected, and this runs once per node of a
|
||
|
|
// whole-tree walk.
|
||
|
|
if (UObject* Content = RawObject{ Slot, STR("PanelSlot") }.TryObjectProperty(STR("Content")))
|
||
|
|
{
|
||
|
|
Out.push_back(Content);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Property read first, ProcessEvent second. Use this anywhere you
|
||
|
|
// just want the children and don't care which route produced them.
|
||
|
|
auto GetChildObjects() -> std::vector<UObject*>
|
||
|
|
{
|
||
|
|
std::vector<UObject*> Out;
|
||
|
|
if (TryGetChildObjects(Out)) return Out;
|
||
|
|
|
||
|
|
for (Widget& Child : GetAllChildren())
|
||
|
|
{
|
||
|
|
if (UObject* O = Child.Get()) Out.push_back(O);
|
||
|
|
}
|
||
|
|
return Out;
|
||
|
|
}
|
||
|
|
|
||
|
|
protected:
|
||
|
|
PanelWidget(UObject* Obj, UClass* Expected, const TCHAR* Tag)
|
||
|
|
: Widget(Obj, Expected, Tag) {}
|
||
|
|
};
|
||
|
|
}
|