using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Enums; using KamiToolKit.Timelines; namespace KamiToolKit.Nodes; public abstract class ListNode : SimpleComponentNode; /// Note, automatically inserts buttons to fill the set height, please ensure option count is greater than button count. public abstract unsafe class ButtonListNode : ListNode { public readonly NineGridNode BackgroundNode; public readonly ResNode ContainerNode; public readonly ScrollBarNode ScrollBarNode; public List Nodes = []; protected ButtonListNode() { SetInternalComponentType(ComponentType.Base); BackgroundNode = new SimpleNineGridNode { TexturePath = "ui/uld/ListB.tex", TextureCoordinates = new Vector2(0.0f, 0.0f), TextureSize = new Vector2(32.0f, 32.0f), TopOffset = 10, BottomOffset = 12, LeftOffset = 10, RightOffset = 10, }; BackgroundNode.AttachNode(this); ContainerNode = new ResNode { NodeFlags = NodeFlags.Visible | NodeFlags.Clip, }; ContainerNode.AttachNode(this); ScrollBarNode = new ScrollBarNode { Position = new Vector2(0.0f, 9.0f), Size = new Vector2(8.0f, 0.0f), OnValueChanged = OnScrollUpdate, HideWhenDisabled = true, }; ScrollBarNode.AttachNode(this); BuildTimelines(); ContainerNode.AddEvent(AtkEventType.MouseWheel, OnMouseWheel); } protected override void Dispose(bool disposing, bool isNativeDestructor) { if (disposing) { if (isFocusSet && !isNativeDestructor) { if (ParentAddon is not null) { ClearFocusable(ParentAddon); } } base.Dispose(disposing, isNativeDestructor); } } public T? SelectedOption { get; set { field = value; UpdateSelected(); } } public List? Options { get; set { field = value; RebuildNodeList(); } } protected float NodeHeight { get; set; } = 22.0f; private int ButtonCount { get; set; } public int MaxButtons { get; set { field = value; RebuildNodeList(); } } = 5; public int CurrentStartIndex { get; set; } public Action? OnOptionSelected { get; set; } protected override void OnSizeChanged() { base.OnSizeChanged(); BackgroundNode.Size = Size; ContainerNode.Size = new Vector2(Width - 25.0f, Height); foreach (var buttonNode in Nodes) { buttonNode.Width = Width - 25.0f; } ScrollBarNode.X = Width - 17.0f; } private void OnScrollUpdate(int scrollPosition) { var index = scrollPosition / 22.0f; CurrentStartIndex = (int)index; UpdateNodes(); } private void OnMouseWheel(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) { CurrentStartIndex -= atkEventData->MouseData.WheelDirection; UpdateNodes(); ScrollBarNode.ScrollPosition = (int)(CurrentStartIndex * NodeHeight + 9.0f); atkEvent->SetEventIsHandled(); } private void RebuildNodeList() { foreach (var button in Nodes) { button.DetachNode(); button.Dispose(); } Nodes.Clear(); ButtonCount = Math.Min(MaxButtons, Options?.Count ?? 0); var height = ButtonCount * NodeHeight + 24.0f; Height = height; BackgroundNode.Height = height; ContainerNode.Height = height; ScrollBarNode.Height = height - 23.0f; foreach (var index in Enumerable.Range(0, ButtonCount)) { var newButton = new ListButtonNode { NodeId = (uint)index, Size = new Vector2(Width - 25.0f, NodeHeight), Position = new Vector2(8.0f, NodeHeight * index + 9.0f), String = $"Button {index}", OnClick = () => OnOptionClick(index), }; Nodes.Add(newButton); newButton.AttachNode(ContainerNode); } RecalculateScrollParams(); UpdateNodes(); } public void RecalculateScrollParams() { if (Options is not null) { ScrollBarNode.UpdateScrollParams((int)ScrollBarNode.Height, (int)(Options.Count * NodeHeight)); } } protected virtual void OnOptionClick(int nodeId) { if (Options is null) return; SelectedOption = Options[nodeId + CurrentStartIndex]; OnOptionSelected?.Invoke(Options[nodeId + CurrentStartIndex]); UpdateSelected(); } private void UpdateSelected() { if (Options is null) return; foreach (var index in Enumerable.Range(0, ButtonCount)) { var option = Options[index + CurrentStartIndex]; Nodes[index].Selected = SelectedOption?.Equals(option) ?? false; Nodes[index].String = GetLabelForOption(option); } } protected abstract string GetLabelForOption(T option); protected void UpdateNodes() { if (Options is null) return; var maxStartIndex = Options.Count - Nodes.Count; var max = Math.Max(0, maxStartIndex); CurrentStartIndex = Math.Clamp(CurrentStartIndex, 0, max); UpdateSelected(); } public void SelectDefaultOption() { if (Options is not null && Options.Count > 0) { SelectedOption = Options.First(); } } public void Show() { IsVisible = true; AddDrawFlags(DrawFlags.RenderOnTop); if (ParentAddon is not null) { SetFocusable(ParentAddon); } } public void Hide() { IsVisible = false; RemoveDrawFlags(DrawFlags.RenderOnTop); if (ParentAddon is not null) { ClearFocusable(ParentAddon); } } public void Toggle(bool newState) { if (newState) { Show(); } else { Hide(); } } private bool isFocusSet; public void SetFocusable(AtkUnitBase* addon) { foreach (ref var focusableNode in addon->AdditionalFocusableNodes) { if (focusableNode.Value is null) { focusableNode = ResNode; isFocusSet = true; } } } public void ClearFocusable(AtkUnitBase* addon) { foreach (ref var focusableNode in addon->AdditionalFocusableNodes) { if (focusableNode.Value == ResNode) { focusableNode = null; isFocusSet = false; } } } private void BuildTimelines() { AddTimeline(new TimelineBuilder() .BeginFrameSet(1, 29) .AddLabel(1, 17, AtkTimelineJumpBehavior.Start, 0) .AddLabel(9, 0, AtkTimelineJumpBehavior.PlayOnce, 0) .AddLabel(10, 18, AtkTimelineJumpBehavior.Start, 0) .AddLabel(19, 0, AtkTimelineJumpBehavior.PlayOnce, 0) .AddLabel(20, 7, AtkTimelineJumpBehavior.Start, 0) .AddLabel(29, 0, AtkTimelineJumpBehavior.PlayOnce, 0) .EndFrameSet() .Build() ); } }