262 lines
7.3 KiB
C#
262 lines
7.3 KiB
C#
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<T> : ListNode {
|
|
|
|
public readonly NineGridNode BackgroundNode;
|
|
public readonly ResNode ContainerNode;
|
|
public readonly ScrollBarNode ScrollBarNode;
|
|
public List<ListButtonNode> 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<T>? 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<T>? 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()
|
|
);
|
|
}
|
|
}
|