Initial commit: AetherBags + KamiToolKit for FC Gitea
Debug Build and Test / Build against Latest Dalamud (push) Has been cancelled
Debug Build and Test / Build against Staging Dalamud (push) Has been cancelled

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-08 14:46:31 -05:00
commit 8db4ce6094
375 changed files with 34124 additions and 0 deletions
@@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
using KamiToolKit.Premade.Nodes;
namespace KamiToolKit.Premade.Addons;
public class ListConfigAddon<T, TU, TV> : NativeAddon where T: class where TV : ConfigNode<T>, new() where TU : ListItemNode<T>, new() {
private ModifyListNode<T, TU>? selectionListNode;
private VerticalLineNode? separatorLine;
private TV? configNode;
private TextNode? nothingSelectedTextNode;
protected override unsafe void OnSetup(AtkUnitBase* addon) {
selectionListNode = new ModifyListNode<T, TU> {
Position = ContentStartPosition,
Size = new Vector2(250.0f, ContentSize.Y),
SortOptions = SortOptions,
Options = Options,
SelectionChanged = SelectionChanged,
AddNewEntry = OnAddClicked,
RemoveEntry = OnRemoveClicked,
ItemComparer = ItemComparer,
IsSearchMatch = OnSearchUpdated,
ItemSpacing = ItemSpacing,
};
selectionListNode.AttachNode(this);
separatorLine = new VerticalLineNode {
Position = ContentStartPosition + new Vector2(250.0f + 8.0f, 0.0f),
Size = new Vector2(4.0f, ContentSize.Y),
};
separatorLine.AttachNode(this);
nothingSelectedTextNode = new TextNode {
Position = ContentStartPosition + new Vector2(250.0f + 16.0f, 0.0f),
Size = ContentSize - new Vector2(250.0f + 16.0f, 0.0f),
AlignmentType = AlignmentType.Center,
TextFlags = TextFlags.WordWrap | TextFlags.MultiLine,
FontSize = 14,
LineSpacing = 22,
FontType = FontType.Axis,
String = "Please select an option on the left",
TextColor = ColorHelper.GetColor(1),
};
nothingSelectedTextNode.AttachNode(this);
configNode = new TV {
Position = ContentStartPosition + new Vector2(250.0f + 16.0f, 0.0f),
Size = ContentSize - new Vector2(250.0f + 16.0f, 0.0f),
OnConfigChanged = option => EditCompleted?.Invoke(option),
IsVisible = false,
};
configNode.AttachNode(this);
}
public required ModifyListNode<T, TU>.ItemCompareDelegate? ItemComparer {
get;
init {
field = value;
selectionListNode?.ItemComparer = value;
}
}
public required ModifyListNode<T, TU>.IsSearchMatchDelegate? IsSearchMatch {
get;
init {
field = value;
selectionListNode?.IsSearchMatch = value;
}
}
private void OnAddClicked() {
AddClicked?.Invoke(this);
selectionListNode?.RefreshList();
}
private void OnRemoveClicked(T listItem) {
RemoveClicked?.Invoke(this, listItem);
SelectionChanged(null);
selectionListNode?.RefreshList();
}
private void SelectionChanged(T? listItem) {
SetConfigNodeItem(listItem);
}
private bool OnSearchUpdated(T obj, string searchString) {
SelectItem(null);
return IsSearchMatch?.Invoke(obj, searchString) ?? false;
}
private void SetConfigNodeItem(T? configItem) {
if (configNode is null) return;
if (nothingSelectedTextNode is null) return;
configNode.ConfigurationOption = configItem;
configNode.IsVisible = configNode.ConfigurationOption is not null;
nothingSelectedTextNode.IsVisible = configNode.ConfigurationOption is null;
}
public void RefreshList()
=> selectionListNode?.RefreshList();
public void SelectItem(T? listItem)
=> SelectionChanged(listItem);
public List<string>? SortOptions {
get;
set {
field = value;
selectionListNode?.SortOptions = value;
}
} = ["Alphabetical", "Id"];
public required List<T> Options { get;
set {
field = value;
selectionListNode?.Options = value;
}
} = [];
public float ItemSpacing {
get;
set {
field = value;
selectionListNode?.ItemSpacing = value;
}
}
public Action<ListConfigAddon<T, TU, TV>>? AddClicked {
get;
set {
field = value;
selectionListNode?.AddNewEntry = () => {
value?.Invoke(this);
};
}
}
public Action<ListConfigAddon<T, TU, TV>, T>? RemoveClicked {
get;
set {
field = value;
selectionListNode?.RemoveEntry = entry => {
value?.Invoke(this, entry);
};
}
}
public Action<T>? EditCompleted { get; set; }
}
@@ -0,0 +1,93 @@
using System;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Nodes;
using Lumina.Text.ReadOnly;
namespace KamiToolKit.Premade.Color;
public class ColorEditNode : SimpleOverlayNode {
private readonly ColorPreviewNode previewNode;
private readonly TextNode labelNode;
private ColorPickerAddon? colorPicker = new() {
InternalName = "ColorPicker",
Title = "Color Picker",
};
public ColorEditNode() {
DisableCollisionNode = true;
previewNode = new ColorPreviewNode();
previewNode.AttachNode(this);
labelNode = new TextNode {
AlignmentType = AlignmentType.Left,
};
labelNode.AttachNode(this);
previewNode.CollisionNode.ShowClickableCursor = true;
previewNode.CollisionNode.AddEvent(AtkEventType.MouseClick, OnClicked);
labelNode.ShowClickableCursor = true;
labelNode.AddEvent(AtkEventType.MouseClick, OnClicked);
}
protected override void Dispose(bool disposing, bool isNativeDestructor) {
base.Dispose(disposing, isNativeDestructor);
colorPicker?.Dispose();
colorPicker = null;
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
previewNode.Size = new Vector2(Height - 6.0f, Height - 6.0f);
previewNode.Position = new Vector2(3.0f, 3.0f);
labelNode.Size = new Vector2(Width - Height - 12.0f, Height);
labelNode.Position = new Vector2(previewNode.Bounds.Right + 12.0f, 0.0f);
}
private void OnClicked() {
var originalColor = CurrentColor;
colorPicker?.DefaultColor = DefaultColor;
colorPicker?.InitialColor = CurrentColor;
colorPicker?.OnColorPreviewed = color => {
previewNode.Color = color;
CurrentColor = color;
OnColorPreviewed?.Invoke(color);
};
colorPicker?.OnColorCancelled = () => {
CurrentColor = originalColor;
OnColorCancelled?.Invoke();
};
colorPicker?.OnColorConfirmed = color => {
CurrentColor = color;
OnColorConfirmed?.Invoke(color);
};
colorPicker?.Toggle();
}
public Vector4 CurrentColor {
get => previewNode.Color;
set => previewNode.Color = value;
}
public ReadOnlySeString String {
get => labelNode.String;
set => labelNode.String = value;
}
public Vector4? DefaultColor { get; set; }
public Action? OnColorCancelled { get; set; }
public Action<Vector4>? OnColorPreviewed { get; set; }
public Action<Vector4>? OnColorConfirmed { get; set; }
}
@@ -0,0 +1,135 @@
using System;
using System.Numerics;
using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Nodes;
namespace KamiToolKit.Premade.Color;
public class ColorPickerAddon : NativeAddon {
private ColorPickerWidget? colorPicker;
private HorizontalLineNode? horizontalLine;
private TextButtonNode? confirmButton;
private ColorOptionTextButtonNode? defaultColorPreview;
private TextButtonNode? cancelButton;
private bool isCancelClicked;
private Vector4 initialRgba;
private ColorHelpers.HsvaColor initialHsva;
protected override unsafe void OnSetup(AtkUnitBase* addon) {
SetWindowSize(new Vector2(400.0f, 425.0f));
initialHsva = InitialHsvaColor;
initialRgba = ColorHelpers.HsvToRgb(initialHsva);
colorPicker = new ColorPickerWidget {
Position = ContentStartPosition,
Size = ContentSize,
};
colorPicker.AttachNode(this);
colorPicker.ColorPreviewed += hsva => OnHsvaColorPreviewed?.Invoke(hsva);
colorPicker.RgbaColorPreviewed += rgba => OnColorPreviewed?.Invoke(rgba);
colorPicker.SetColor(initialRgba);
horizontalLine = new HorizontalLineNode {
Position = ContentStartPosition + new Vector2(2.0f, ContentSize.Y - 40.0f),
Size = new Vector2(ContentSize.X - 4.0f, 2.0f),
};
horizontalLine.AttachNode(this);
confirmButton = new TextButtonNode {
Position = ContentStartPosition + new Vector2(0.0f, ContentSize.Y - 24.0f),
Size = new Vector2(100.0f, 24.0f),
String = "Confirm",
OnClick = OnConfirmClicked,
};
confirmButton.AttachNode(this);
if (DefaultHsvaColor is { } defaultColor) {
defaultColorPreview = new ColorOptionTextButtonNode {
Size = new Vector2(100.0f, 24.0f),
Position = ContentStartPosition + new Vector2(ContentSize.X / 2.0f - 50.0f, ContentSize.Y - 24.0f),
String = "Default",
OnClick = OnDefaultClicked,
DefaultHsvaColor = defaultColor,
};
defaultColorPreview.AttachNode(this);
}
cancelButton = new TextButtonNode {
Position = ContentStartPosition + new Vector2(ContentSize.X - 100.0f, ContentSize.Y - 24.0f),
Size = new Vector2(100.0f, 24.0f),
String = "Cancel",
OnClick = OnCancelClicked,
};
cancelButton.AttachNode(this);
}
protected override unsafe void OnHide(AtkUnitBase* addon) {
if (!isCancelClicked) {
OnHsvaColorPreviewed?.Invoke(initialHsva);
OnColorPreviewed?.Invoke(initialRgba);
OnColorCancelled?.Invoke();
}
}
private void OnConfirmClicked() {
if (colorPicker is null) return;
var rgba = ColorHelpers.HsvToRgb(colorPicker.CurrentColor);
OnColorConfirmed?.Invoke(rgba);
OnHsvaColorConfirmed?.Invoke(colorPicker.CurrentColor);
isCancelClicked = true;
Close();
}
private void OnDefaultClicked() {
if (colorPicker is null) return;
if (DefaultHsvaColor is { } defaultColor) {
colorPicker.SetColor(defaultColor);
}
}
private void OnCancelClicked() {
isCancelClicked = true;
OnHsvaColorPreviewed?.Invoke(initialHsva);
OnColorPreviewed?.Invoke(initialRgba);
OnColorCancelled?.Invoke();
Close();
}
public Action<Vector4>? OnColorPreviewed { get; set; }
public Action<ColorHelpers.HsvaColor>? OnHsvaColorPreviewed { get; set; }
public Action<Vector4>? OnColorConfirmed { get; set; }
public Action<ColorHelpers.HsvaColor>? OnHsvaColorConfirmed { get; set; }
public Action? OnColorCancelled { get; set; }
public ColorHelpers.HsvaColor? DefaultHsvaColor { get; set; }
public Vector4? DefaultColor {
get;
set {
field = value;
DefaultHsvaColor = value is null ? null : ColorHelpers.RgbaToHsv(value.Value);
}
}
public Vector4 InitialColor {
get => ColorHelpers.HsvToRgb(InitialHsvaColor);
set => InitialHsvaColor = ColorHelpers.RgbaToHsv(value);
}
public ColorHelpers.HsvaColor InitialHsvaColor { get; set; } =
ColorHelpers.RgbaToHsv(new Vector4(1.0f, 0.0f, 0.0f, 1.0f));
}
@@ -0,0 +1,173 @@
using System;
using System.Numerics;
using Dalamud.Interface;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
using KamiToolKit.Premade.Nodes;
namespace KamiToolKit.Premade.Color;
public class ColorPickerWidget : SimpleComponentNode {
public readonly ColorRingWithSquareNode ColorPickerNode;
public readonly AlphaBarNode AlphaBarNode;
public readonly ColorPreviewWithInput ColorPreviewWithInput;
public ColorHelpers.HsvaColor CurrentColor { get; private set; }
public Action<ColorHelpers.HsvaColor>? ColorPreviewed;
public Action<Vector4>? RgbaColorPreviewed;
private int batchDepth;
private bool previewDirty;
public ColorPickerWidget() {
ColorPickerNode = new ColorRingWithSquareNode {
OnHueChanged = SetHue,
OnSaturationChanged = SetSaturation,
OnValueChanged = SetValue,
};
ColorPickerNode.AttachNode(this);
AlphaBarNode = new AlphaBarNode {
OnAlphaChanged = SetAlpha,
};
AlphaBarNode.AttachNode(this);
ColorPreviewWithInput = new ColorPreviewWithInput {
OnHsvaColorChanged = newColor => {
using (BeginBatchUpdate()) {
SetHue(newColor.H);
SetSaturation(newColor.S);
SetValue(newColor.V);
SetAlpha(newColor.A);
}
},
};
ColorPreviewWithInput.AttachNode(this);
CurrentColor = ColorHelpers.RgbaToHsv(new Vector4(1.0f, 0.0f, 0.0f, 1.0f));
using (BeginBatchUpdate()) {
SetHue(CurrentColor.H);
SetSaturation(CurrentColor.S);
SetValue(CurrentColor.V);
SetAlpha(CurrentColor.A);
}
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
var mainWidgetWidth = Width * 3.0f / 4.0f;
ColorPickerNode.Size = new Vector2(mainWidgetWidth, mainWidgetWidth);
AlphaBarNode.Size = new Vector2(Width / 16.0f, mainWidgetWidth - 60.0f);
AlphaBarNode.Position = new Vector2(mainWidgetWidth + (Width - mainWidgetWidth) / 3.0f - AlphaBarNode.Width / 2.0f, 30.0f);
ColorPreviewWithInput.Size = new Vector2(150.0f, 32.0f);
ColorPreviewWithInput.Position = new Vector2(Width / 2.0f - 75.0f, ColorPickerNode.Y + ColorPickerNode.Height - 1.0f);
}
private IDisposable BeginBatchUpdate() {
batchDepth++;
return new BatchToken(this);
}
internal void EndBatchUpdate() {
batchDepth--;
if (batchDepth <= 0) {
batchDepth = 0;
if (previewDirty) {
previewDirty = false;
RaisePreview();
}
}
}
private void RaisePreviewMaybe() {
if (batchDepth > 0) {
previewDirty = true;
return;
}
RaisePreview();
}
private void RaisePreview() {
var hsva = CurrentColor;
ColorPreviewed?.Invoke(hsva);
RgbaColorPreviewed?.Invoke(ColorHelpers.HsvToRgb(hsva));
}
public void SetAlpha(float alpha) {
CurrentColor = CurrentColor with { A = alpha };
ColorPreviewWithInput.ColorHsva = CurrentColor;
AlphaBarNode.ColorHsva = CurrentColor;
RaisePreviewMaybe();
}
public void SetHue(float hue) {
CurrentColor = CurrentColor with { H = hue };
ColorPickerNode.RotationDegrees = hue * 360.0f;
ColorPickerNode.SelectorColor = CurrentColor;
ColorPickerNode.SquareColor = CurrentColor with { S = 1.0f, V = 1.0f };
ColorPreviewWithInput.ColorHsva = CurrentColor;
AlphaBarNode.ColorHsva = CurrentColor;
RaisePreviewMaybe();
}
public void SetSaturation(float saturation) {
CurrentColor = CurrentColor with { S = saturation };
ColorPreviewWithInput.ColorHsva = CurrentColor;
ColorPickerNode.SelectorColor = CurrentColor;
ColorPickerNode.SquareColor = CurrentColor;
ColorPickerNode.SquareSaturationValue = CurrentColor;
AlphaBarNode.ColorHsva = CurrentColor;
RaisePreviewMaybe();
}
public void SetValue(float value) {
CurrentColor = CurrentColor with { V = value };
ColorPreviewWithInput.ColorHsva = CurrentColor;
ColorPickerNode.SelectorColor = CurrentColor;
ColorPickerNode.SquareColor = CurrentColor;
ColorPickerNode.SquareSaturationValue = CurrentColor;
AlphaBarNode.ColorHsva = CurrentColor;
RaisePreviewMaybe();
}
public void SetColor(Vector4 color) {
var converted = ColorHelpers.RgbaToHsv(color);
using (BeginBatchUpdate()) {
SetHue(converted.H);
SetSaturation(converted.S);
SetValue(converted.V);
SetAlpha(converted.A);
}
}
public void SetColor(ColorHelpers.HsvaColor color) {
using (BeginBatchUpdate()) {
SetHue(color.H);
SetSaturation(color.S);
SetValue(color.V);
SetAlpha(color.A);
}
}
}
@@ -0,0 +1,55 @@
using System.Drawing;
using System.Numerics;
using Dalamud.Interface;
using KamiToolKit.Classes;
using KamiToolKit.Enums;
using KamiToolKit.Nodes;
namespace KamiToolKit.Premade.Color;
public class ColorPreviewNode : SimpleComponentNode {
public readonly BackgroundImageNode SelectedColorPreviewNode;
public readonly ImGuiImageNode AlphaLayerPreviewNode;
public readonly BackgroundImageNode SelectedColorPreviewBorderNode;
public ColorPreviewNode() {
SelectedColorPreviewBorderNode = new BackgroundImageNode {
Color = KnownColor.White.Vector(),
};
SelectedColorPreviewBorderNode.AttachNode(this);
AlphaLayerPreviewNode = new ImGuiImageNode {
TexturePath = DalamudInterface.Instance.GetAssetPath("alpha_background.png"),
WrapMode = WrapMode.Tile,
};
AlphaLayerPreviewNode.AttachNode(this);
SelectedColorPreviewNode = new BackgroundImageNode {
Color = new Vector4(1.0f, 0.0f, 0.0f, 1.0f),
};
SelectedColorPreviewNode.AttachNode(this);
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
SelectedColorPreviewBorderNode.Size = new Vector2(Height - 4.0f, Width - 4.0f);
SelectedColorPreviewBorderNode.Position = new Vector2(2.0f, 2.0f);
AlphaLayerPreviewNode.Size = new Vector2(Height - 6.0f, Width - 6.0f);
AlphaLayerPreviewNode.Position = new Vector2(3.0f, 3.0f);
SelectedColorPreviewNode.Size = new Vector2(Height - 6.0f, Width - 6.0f);
SelectedColorPreviewNode.Position = new Vector2(3.0f, 3.0f);
}
public override Vector4 Color {
get => SelectedColorPreviewNode.Color;
set => SelectedColorPreviewNode.Color = value;
}
public override ColorHelpers.HsvaColor ColorHsva {
get => SelectedColorPreviewNode.ColorHsva;
set => SelectedColorPreviewNode.ColorHsva = value;
}
}
@@ -0,0 +1,88 @@
using System;
using System.Globalization;
using System.Numerics;
using Dalamud.Interface;
using KamiToolKit.Nodes;
using Lumina.Text.ReadOnly;
namespace KamiToolKit.Premade.Color;
public class ColorPreviewWithInput : SimpleComponentNode {
public readonly ColorPreviewNode ColorPreviewNode;
public readonly TextInputNode ColorInputNode;
public ColorPreviewWithInput() {
ColorPreviewNode = new ColorPreviewNode();
ColorPreviewNode.AttachNode(this);
ColorInputNode = new TextInputNode {
AutoSelectAll = true,
OnInputComplete = OnTextInputComplete,
};
ColorInputNode.AttachNode(this);
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
ColorPreviewNode.Size = new Vector2(Height, Height);
ColorPreviewNode.Position = Vector2.Zero;
ColorInputNode.Size = new Vector2(Width - Height - 8.0f, Height - 2.0f);
ColorInputNode.Position = new Vector2(Height + 8.0f, 1.0f);
}
public Action<ColorHelpers.HsvaColor>? OnHsvaColorChanged { get; set; }
public Action<Vector4>? OnColorChanged { get; set; }
public ReadOnlySeString String {
get => ColorInputNode.String;
set => ColorInputNode.String = value;
}
public override Vector4 Color {
get => ColorPreviewNode.Color;
set {
ColorPreviewNode.Color = value;
UpdateColorText();
}
}
public override ColorHelpers.HsvaColor ColorHsva {
get => ColorPreviewNode.ColorHsva;
set {
ColorPreviewNode.ColorHsva = value;
UpdateColorText();
}
}
private void OnTextInputComplete(ReadOnlySeString obj) {
var str = obj.ToString();
if (string.IsNullOrEmpty(str) || !str.StartsWith('#')) return;
var hexString = str.TrimStart('#');
// Allow #RRGGBB and #RRGGBBAA only
if (hexString.Length != 6 && hexString.Length != 8) return;
const NumberStyles style = NumberStyles.HexNumber;
var culture = CultureInfo.InvariantCulture;
if (!byte.TryParse(hexString[0..2], style, culture, out var r)) return;
if (!byte.TryParse(hexString[2..4], style, culture, out var g)) return;
if (!byte.TryParse(hexString[4..6], style, culture, out var b)) return;
byte a = 255;
if (hexString.Length == 8 && !byte.TryParse(hexString[6..8], style, culture, out a)) return;
var newColor = new Vector4(r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f);
Color = newColor;
OnColorChanged?.Invoke(newColor);
OnHsvaColorChanged?.Invoke(ColorHelpers.RgbaToHsv(newColor));
}
private void UpdateColorText()
=> ColorInputNode.String = $"#{(int)(Color.X * 255):X2}{(int)(Color.Y * 255):X2}{(int)(Color.Z * 255):X2}{(int)(Color.W * 255):X2}";
}
@@ -0,0 +1,201 @@
using System;
using System.Numerics;
using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Enums;
using KamiToolKit.Nodes;
namespace KamiToolKit.Premade.Color;
public unsafe class ColorRingWithSquareNode : SimpleComponentNode {
public readonly ColorSquareNode ColorSquareNode;
public readonly ImGuiImageNode ColorRingNode;
public readonly ImGuiImageNode ColorRingSelectorNode;
private bool isRingDrag;
private bool isSquareDrag;
private readonly ViewportEventListener eventListener;
public ColorRingWithSquareNode() {
eventListener = new ViewportEventListener(SquareEventHandler);
ColorSquareNode = new ColorSquareNode {
DrawFlags = DrawFlags.UseTransformedCollision,
};
ColorSquareNode.AttachNode(this);
ColorRingNode = new ImGuiImageNode {
TexturePath = DalamudInterface.Instance.GetAssetPath("color_ring.png"),
FitTexture = true,
ImageNodeFlags = ImageNodeFlags.FlipV,
};
ColorRingNode.AttachNode(this);
ColorRingSelectorNode = new ImGuiImageNode {
TexturePath = DalamudInterface.Instance.GetAssetPath("color_ring_selector.png"),
FitTexture = true,
MultiplyColor = new Vector3(1.0f, 0.0f, 0.0f),
};
ColorRingSelectorNode.AttachNode(this);
AddEvent(AtkEventType.MouseDown, OnMouseDown);
AddEvent(AtkEventType.MouseUp, OnMouseUp);
AddEvent(AtkEventType.MouseMove, OnMouseMove);
AddEvent(AtkEventType.MouseOut, OnMouseOut);
}
protected override void Dispose(bool disposing, bool isNativeDestructor) {
if (disposing) {
base.Dispose(disposing, isNativeDestructor);
eventListener.Dispose();
}
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
ColorSquareNode.Size = Size / 2.0f - new Vector2(24.0f, 24.0f);
ColorSquareNode.Position = Size / 4.0f + new Vector2(12.0f, 12.0f);
ColorSquareNode.RotationDegrees = 45.0f;
ColorRingNode.Size = Size;
ColorRingSelectorNode.Size = Size;
ColorRingSelectorNode.Origin = Size / 2.0f;
}
private bool IsRingClicked(AtkEventData* data) {
var clickPosition = data->MousePosition;
var scale = ParentAddon is not null ? ParentAddon->Scale : 1.0f;
var center = ColorRingNode.ScreenPosition + ColorRingNode.Size * scale / 2.0f;
var distance = Vector2.Distance(clickPosition, center);
var scaledDistance = distance / (Width * scale / 256.0f);
return scaledDistance is >= 82.0f and <= 99.0f;
}
private float GetRingClickAngle(AtkEventData* data) {
var clickPosition = data->MousePosition;
var scale = ParentAddon is not null ? ParentAddon->Scale : 1.0f;
var center = ColorRingNode.ScreenPosition + ColorRingNode.Size * scale / 2.0f;
var relativePosition = clickPosition - center;
var calculatedAngle = MathF.Atan2(relativePosition.Y, relativePosition.X) * 180.0f / MathF.PI;
return calculatedAngle;
}
private void OnMouseDown(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
if (ColorSquareNode.CheckCollision(atkEventData)) {
UpdateSquareColor(atkEventData->MousePosition);
if (!isSquareDrag) {
isSquareDrag = true;
eventListener.AddEvent(AtkEventType.MouseMove, ColorSquareNode);
eventListener.AddEvent(AtkEventType.MouseUp, ColorSquareNode);
}
}
if (IsRingClicked(atkEventData)) {
isRingDrag = true;
UpdateRingColor(atkEventData);
}
}
private void OnMouseMove(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
if (isRingDrag && !isSquareDrag) {
UpdateRingColor(atkEventData);
}
}
private void OnMouseUp() {
isRingDrag = false;
isSquareDrag = false;
}
private void OnMouseOut() {
isRingDrag = false;
isSquareDrag = false;
}
private void UpdateRingColor(AtkEventData* data) {
var angle = GetRingClickAngle(data);
if (angle < 0) {
angle += 360.0f;
}
OnHueChanged?.Invoke(angle / 360.0f);
}
private void UpdateSquareColor(Vector2 clickPosition) {
// Note: ColorSquareNode.ScreenPosition changes as the node rotates
// However, Position does not change
var scale = ParentAddon is not null ? ParentAddon->Scale : 1.0f;
var center = ScreenPosition + (ColorSquareNode.Position + ColorSquareNode.Origin) * scale;
var relativePosition = clickPosition - center;
var rotatedPoint = RotatePoint(relativePosition / scale, Vector2.Zero, -ColorSquareNode.RotationDegrees) / ColorSquareNode.Scale;
var xClamped = Math.Clamp(rotatedPoint.X, -ColorSquareNode.Width / 2, ColorSquareNode.Width / 2);
var yClamped = Math.Clamp(rotatedPoint.Y, -ColorSquareNode.Height / 2, ColorSquareNode.Height / 2);
ColorSquareNode.ColorDotPosition = new Vector2(xClamped, yClamped) + ColorSquareNode.Origin;
var saturation = ColorSquareNode.ColorDotPosition.X / ColorSquareNode.Width;
var lightness = 1 - ColorSquareNode.ColorDotPosition.Y / ColorSquareNode.Height;
OnSaturationChanged?.Invoke(saturation);
OnValueChanged?.Invoke(lightness);
}
private void SquareEventHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
if (eventType is AtkEventType.MouseMove && isSquareDrag && !isRingDrag) {
UpdateSquareColor(new Vector2(atkEventData->MouseData.PosX, atkEventData->MouseData.PosY));
}
if (eventType is AtkEventType.MouseUp) {
isSquareDrag = false;
eventListener.RemoveEvent(AtkEventType.MouseMove);
eventListener.RemoveEvent(AtkEventType.MouseUp);
}
}
private static Vector2 RotatePoint(Vector2 pointToRotate, Vector2 centerPoint, float angleInDegrees) {
var angleInRadians = angleInDegrees * (MathF.PI / 180);
var cosTheta = MathF.Cos(angleInRadians);
var sinTheta = MathF.Sin(angleInRadians);
return new Vector2 {
X = cosTheta * (pointToRotate.X - centerPoint.X) - sinTheta * (pointToRotate.Y - centerPoint.Y) + centerPoint.X,
Y = sinTheta * (pointToRotate.X - centerPoint.X) + cosTheta * (pointToRotate.Y - centerPoint.Y) + centerPoint.Y,
};
}
public Action<float>? OnHueChanged { get; init; }
public Action<float>? OnSaturationChanged { get; init; }
public Action<float>? OnValueChanged { get; init; }
public override float RotationDegrees {
get => ColorSquareNode.RotationDegrees;
set {
ColorSquareNode.RotationDegrees = value + 45.0f;
ColorRingSelectorNode.RotationDegrees = value;
}
}
public ColorHelpers.HsvaColor SelectorColor {
get => ColorRingSelectorNode.MultiplyColorHsva;
set => ColorRingSelectorNode.MultiplyColorHsva = value;
}
public ColorHelpers.HsvaColor SquareColor {
get => ColorSquareNode.MultiplyColorHsva;
set => ColorSquareNode.MultiplyColorHsva = value with { S = 1.0f, V = 1.0f };
}
public ColorHelpers.HsvaColor SquareSaturationValue {
get => ColorSquareNode.MultiplyColorHsva;
set => ColorSquareNode.ColorDotPosition = new Vector2(ColorSquareNode.Width * value.S, ColorSquareNode.Height - ColorSquareNode.Height * value.V);
}
}
@@ -0,0 +1,65 @@
using System.Numerics;
using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
namespace KamiToolKit.Premade.Color;
public class ColorSquareNode : SimpleComponentNode {
public readonly ImGuiImageNode WhiteGradientNode;
public readonly ImGuiImageNode ColorGradientNode;
public readonly ImGuiImageNode BlackGradientNode;
public readonly ImGuiImageNode ColorDotNode;
public ColorSquareNode() {
WhiteGradientNode = new ImGuiImageNode {
TexturePath = DalamudInterface.Instance.GetAssetPath("HorizontalGradient_WhiteToAlpha.png"),
FitTexture = true,
};
WhiteGradientNode.AttachNode(this);
ColorGradientNode = new ImGuiImageNode {
TexturePath = DalamudInterface.Instance.GetAssetPath("HorizontalGradient_WhiteToAlpha.png"),
FitTexture = true,
ImageNodeFlags = ImageNodeFlags.FlipH,
};
ColorGradientNode.AttachNode(this);
BlackGradientNode = new ImGuiImageNode {
TexturePath = DalamudInterface.Instance.GetAssetPath("VerticalGradient_AlphaToBlack.png"),
FitTexture = true,
};
BlackGradientNode.AttachNode(this);
ColorDotNode = new ImGuiImageNode {
TexturePath = DalamudInterface.Instance.GetAssetPath("color_select_dot.png"),
FitTexture = true,
};
ColorDotNode.AttachNode(this);
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
WhiteGradientNode.Size = Size;
ColorGradientNode.Size = Size;
BlackGradientNode.Size = Size;
Origin = Size / 2.0f;
ColorDotNode.Size = new Vector2(16.0f, 16.0f);
ColorDotNode.Origin = ColorDotNode.Size / 2.0f;
ColorDotNode.Position = new Vector2(Width, 0.0f) - ColorDotNode.Origin;
}
public override ColorHelpers.HsvaColor MultiplyColorHsva {
get => ColorGradientNode.MultiplyColorHsva;
set => ColorGradientNode.MultiplyColorHsva = value;
}
public Vector2 ColorDotPosition {
get => ColorDotNode.Position + ColorDotNode.Origin;
set => ColorDotNode.Position = value - ColorDotNode.Origin;
}
}
@@ -0,0 +1,81 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
namespace KamiToolKit.Premade.GenericListItemNodes;
public abstract class GenericListItemNode<T> : ListItemNode<T> {
public override float ItemHeight => 48.0f;
protected readonly IconImageNode IconNode;
protected readonly TextNode LabelTextNode;
protected readonly TextNode SubLabelTextNode;
protected readonly TextNode IdTextNode;
protected GenericListItemNode() {
IconNode = new IconImageNode {
FitTexture = true,
IconId = 60072,
};
IconNode.AttachNode(this);
LabelTextNode = new TextNode {
TextFlags = TextFlags.Ellipsis | TextFlags.Emboss,
FontSize = 14,
LineSpacing = 14,
AlignmentType = AlignmentType.BottomLeft,
TextColor = ColorHelper.GetColor(8),
TextOutlineColor = ColorHelper.GetColor(7),
};
LabelTextNode.AttachNode(this);
SubLabelTextNode = new TextNode {
TextFlags = TextFlags.Ellipsis | TextFlags.Emboss,
FontSize = 12,
LineSpacing = 12,
AlignmentType = AlignmentType.TopLeft,
TextColor = ColorHelper.GetColor(3),
TextOutlineColor = ColorHelper.GetColor(7),
};
SubLabelTextNode.AttachNode(this);
IdTextNode = new TextNode {
TextFlags = TextFlags.Emboss,
FontSize = 10,
AlignmentType = AlignmentType.BottomRight,
TextColor = ColorHelper.GetColor(3),
};
IdTextNode.AttachNode(this);
CollisionNode.ShowClickableCursor = true;
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
IconNode.Size = new Vector2(Height - 4.0f, Height - 4.0f);
IconNode.Position = new Vector2(2.0f, 2.0f);
LabelTextNode.Size = new Vector2(Width - Height - 2.0f - 30.0f, Height / 2.0f);
LabelTextNode.Position = new Vector2(Height + 2.0f, 0.0f);
SubLabelTextNode.Size = new Vector2(Width - Height - 2.0f - 10.0f, Height / 2.0f);
SubLabelTextNode.Position = new Vector2(Height + 2.0f + 10.0f, Height / 2.0f);
IdTextNode.Size = new Vector2(30.0f, Height / 2.0f);
IdTextNode.Position = new Vector2(Width - 30.0f, 0.0f);
}
protected override void SetNodeData(T itemData) {
IconNode.IconId = GetIconId(itemData);
LabelTextNode.String = GetLabelText(itemData);
SubLabelTextNode.String = GetSubLabelText(itemData);
IdTextNode.String = GetId(itemData).ToString() ?? string.Empty;
}
protected abstract uint GetIconId(T data);
protected abstract string GetLabelText(T data);
protected abstract string GetSubLabelText(T data);
protected abstract uint? GetId(T data);
}
@@ -0,0 +1,43 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
namespace KamiToolKit.Premade.GenericListItemNodes;
public abstract class GenericSimpleListItemNode<T> : ListItemNode<T> {
public override float ItemHeight => 48.0f;
protected readonly IconImageNode IconNode;
protected readonly TextNode LabelTextNode;
protected GenericSimpleListItemNode() {
IconNode = new IconImageNode {
FitTexture = true,
IconId = 60072,
};
IconNode.AttachNode(this);
LabelTextNode = new TextNode {
TextFlags = TextFlags.Ellipsis | TextFlags.Emboss,
FontSize = 14,
LineSpacing = 14,
AlignmentType = AlignmentType.Left,
TextColor = ColorHelper.GetColor(8),
TextOutlineColor = ColorHelper.GetColor(7),
};
LabelTextNode.AttachNode(this);
CollisionNode.ShowClickableCursor = true;
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
IconNode.Size = new Vector2(Height - 4.0f, Height - 4.0f);
IconNode.Position = new Vector2(2.0f, 2.0f);
LabelTextNode.Size = new Vector2(Width - IconNode.Width - 6.0f, Height);
LabelTextNode.Position = new Vector2(IconNode.Bounds.Right + 6.0f, 0.0f);
}
}
@@ -0,0 +1,31 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
namespace KamiToolKit.Premade.GenericListItemNodes;
public abstract class GenericStringListItemNode<T> : ListItemNode<T> {
public override float ItemHeight => 24.0f;
protected readonly TextNode StringNode;
protected GenericStringListItemNode() {
StringNode = new TextNode {
TextFlags = TextFlags.Ellipsis | TextFlags.Emboss,
FontSize = 14,
LineSpacing = 14,
AlignmentType = AlignmentType.Left,
TextColor = ColorHelper.GetColor(8),
TextOutlineColor = ColorHelper.GetColor(7),
};
StringNode.AttachNode(this);
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
StringNode.Size = new Vector2(Width - 20.0f, Height);
StringNode.Position = new Vector2(10.0f, 0.0f);
}
}
@@ -0,0 +1,51 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using FFXIVClientStructs.Interop;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
namespace KamiToolKit.Premade.ListItemNodes;
public unsafe class AddonListItemNode : ListItemNode<Pointer<AtkUnitBase>> {
public override float ItemHeight => 48.0f;
protected readonly IconImageNode IconNode;
protected readonly TextNode LabelTextNode;
public AddonListItemNode() {
IconNode = new IconImageNode {
FitTexture = true,
IconId = 60072,
};
IconNode.AttachNode(this);
LabelTextNode = new TextNode {
TextFlags = TextFlags.Ellipsis | TextFlags.Emboss,
FontSize = 14,
LineSpacing = 14,
AlignmentType = AlignmentType.Left,
TextColor = ColorHelper.GetColor(8),
TextOutlineColor = ColorHelper.GetColor(7),
};
LabelTextNode.AttachNode(this);
CollisionNode.ShowClickableCursor = true;
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
IconNode.Size = new Vector2(Height - 4.0f, Height - 4.0f);
IconNode.Position = new Vector2(2.0f, 2.0f);
LabelTextNode.Size = new Vector2(Width - IconNode.Width - 6.0f, Height);
LabelTextNode.Position = new Vector2(IconNode.Bounds.Right + 6.0f, 0.0f);
}
protected override void SetNodeData(Pointer<AtkUnitBase> itemData) {
if (itemData.Value is null) return;
IconNode.IconId = itemData.Value->IsVisible ? (uint) 60071 : 60072;
LabelTextNode.String = itemData.Value->NameString;
}
}
@@ -0,0 +1,51 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
using Lumina.Excel.Sheets;
namespace KamiToolKit.Premade.ListItemNodes;
public class CurrencyListItemNode : ListItemNode<Item> {
public override float ItemHeight => 48.0f;
protected readonly IconImageNode IconNode;
protected readonly TextNode LabelTextNode;
public CurrencyListItemNode() {
IconNode = new IconImageNode {
FitTexture = true,
IconId = 60072,
};
IconNode.AttachNode(this);
LabelTextNode = new TextNode {
TextFlags = TextFlags.Ellipsis | TextFlags.Emboss,
FontSize = 14,
LineSpacing = 14,
AlignmentType = AlignmentType.Left,
TextColor = ColorHelper.GetColor(8),
TextOutlineColor = ColorHelper.GetColor(7),
};
LabelTextNode.AttachNode(this);
CollisionNode.ShowClickableCursor = true;
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
IconNode.Size = new Vector2(Height - 4.0f, Height - 4.0f);
IconNode.Position = new Vector2(2.0f, 2.0f);
LabelTextNode.Size = new Vector2(Width - IconNode.Width - 6.0f, Height);
LabelTextNode.Position = new Vector2(IconNode.Bounds.Right + 6.0f, 0.0f);
}
protected override void SetNodeData(Item itemData) {
if (itemData.RowId is 0) return;
IconNode.IconId = itemData.Icon;
LabelTextNode.String = itemData.Name.ToString();
}
}
@@ -0,0 +1,78 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
using Lumina.Excel.Sheets;
namespace KamiToolKit.Premade.ListItemNodes;
public class ItemListItemNode : ListItemNode<Item> {
public override float ItemHeight => 32.0f;
protected readonly IconImageNode IconNode;
protected readonly TextNode LabelTextNode;
protected readonly TextNode SubLabelTextNode;
protected readonly TextNode IdTextNode;
public ItemListItemNode() {
IconNode = new IconImageNode {
FitTexture = true,
IconId = 60072,
};
IconNode.AttachNode(this);
LabelTextNode = new TextNode {
TextFlags = TextFlags.Ellipsis | TextFlags.Emboss,
FontSize = 14,
LineSpacing = 14,
AlignmentType = AlignmentType.BottomLeft,
TextColor = ColorHelper.GetColor(8),
TextOutlineColor = ColorHelper.GetColor(7),
};
LabelTextNode.AttachNode(this);
SubLabelTextNode = new TextNode {
TextFlags = TextFlags.Ellipsis | TextFlags.Emboss,
FontSize = 12,
LineSpacing = 12,
AlignmentType = AlignmentType.TopLeft,
TextColor = ColorHelper.GetColor(3),
TextOutlineColor = ColorHelper.GetColor(7),
};
SubLabelTextNode.AttachNode(this);
IdTextNode = new TextNode {
TextFlags = TextFlags.Emboss,
FontSize = 10,
AlignmentType = AlignmentType.BottomRight,
TextColor = ColorHelper.GetColor(3),
};
IdTextNode.AttachNode(this);
CollisionNode.ShowClickableCursor = true;
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
IconNode.Size = new Vector2(Height - 4.0f, Height - 4.0f);
IconNode.Position = new Vector2(2.0f, 2.0f);
LabelTextNode.Size = new Vector2(Width - Height - 2.0f - 30.0f, Height / 2.0f);
LabelTextNode.Position = new Vector2(Height + 2.0f, 0.0f);
SubLabelTextNode.Size = new Vector2(Width - Height - 2.0f - 10.0f, Height / 2.0f);
SubLabelTextNode.Position = new Vector2(Height + 2.0f + 10.0f, Height / 2.0f);
IdTextNode.Size = new Vector2(30.0f, Height / 2.0f);
IdTextNode.Position = new Vector2(Width - 30.0f, 0.0f);
}
protected override void SetNodeData(Item itemData) {
if (itemData.RowId is 0) return;
IconNode.IconId = itemData.Icon;
LabelTextNode.String = itemData.Name.ToString();
SubLabelTextNode.String = itemData.ItemSearchCategory.ValueNullable?.Name.ToString() ?? string.Empty;
}
}
@@ -0,0 +1,47 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
using Lumina.Excel.Sheets;
namespace KamiToolKit.Premade.ListItemNodes;
public class StatusListItemNode : ListItemNode<Status> {
public override float ItemHeight => 48.0f;
protected readonly IconImageNode IconImageNode;
protected readonly TextNode StatusLabelNode;
public StatusListItemNode() {
IconImageNode = new IconImageNode {
FitTexture = true,
IconId = 60072,
};
IconImageNode.AttachNode(this);
StatusLabelNode = new TextNode {
TextFlags = TextFlags.Ellipsis | TextFlags.Emboss,
FontSize = 14,
LineSpacing = 14,
AlignmentType = AlignmentType.Left,
TextColor = ColorHelper.GetColor(8),
TextOutlineColor = ColorHelper.GetColor(7),
};
StatusLabelNode.AttachNode(this);
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
IconImageNode.Size = new Vector2((Height - 4.0f) * 0.75f , Height - 4.0f);
IconImageNode.Position = new Vector2(2.0f, 2.0f);
StatusLabelNode.Size = new Vector2(Width - IconImageNode.Width - 6.0f, Height);
StatusLabelNode.Position = new Vector2(IconImageNode.Bounds.Right + 6.0f, 0.0f);
}
protected override void SetNodeData(Status itemData) {
IconImageNode.IconId = itemData.Icon;
StatusLabelNode.String = itemData.Name;
}
}
@@ -0,0 +1,8 @@
using KamiToolKit.Premade.GenericListItemNodes;
namespace KamiToolKit.Premade.ListItemNodes;
public class StringListItemNode : GenericStringListItemNode<string> {
protected override void SetNodeData(string itemData)
=> StringNode.String = itemData;
}
@@ -0,0 +1,89 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
using Lumina.Excel.Sheets;
namespace KamiToolKit.Premade.ListItemNodes;
public class TerritoryTypeListItemNode : ListItemNode<TerritoryType> {
public override float ItemHeight => 64.0f;
private readonly SimpleImageNode territoryImageNode;
private readonly SimpleImageNode placeholderImageNode;
private readonly TextNode territoryTitleNode;
private readonly TextNode territoryDescriptionNode;
private readonly TextNode territoryIdNode;
public TerritoryTypeListItemNode() {
territoryImageNode = new SimpleImageNode {
FitTexture = true,
IsVisible = false,
};
territoryImageNode.AttachNode(this);
placeholderImageNode = new IconImageNode {
FitTexture = true,
IconId = 60072,
};
placeholderImageNode.AttachNode(this);
territoryTitleNode = new TextNode {
TextFlags = TextFlags.Ellipsis,
AlignmentType = AlignmentType.BottomLeft,
String = "None Selected",
};
territoryTitleNode.AttachNode(this);
territoryDescriptionNode = new TextNode {
TextFlags = TextFlags.Ellipsis,
AlignmentType = AlignmentType.TopLeft,
TextColor = ColorHelper.GetColor(2),
};
territoryDescriptionNode.AttachNode(this);
territoryIdNode = new TextNode {
AlignmentType = AlignmentType.Right,
TextColor = ColorHelper.GetColor(3),
};
territoryIdNode.AttachNode(this);
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
territoryImageNode.Size = new Vector2((Height - 4.0f) * 1.777f, Height - 4.0f);
territoryImageNode.Position = new Vector2(2.0f, 2.0f);
territoryIdNode.Size = new Vector2(30.0f, 30.0f);
territoryIdNode.Position = new Vector2(Width - territoryIdNode.Width, 0.0f);
placeholderImageNode.Size = new Vector2(Height - 4.0f, Height - 4.0f);
placeholderImageNode.Position = new Vector2(2.0f, 2.0f);
territoryTitleNode.Size = new Vector2(Width - territoryImageNode.Width - 10.0f - territoryIdNode.Width - 4.0f, Height / 2.0f);
territoryTitleNode.Position = new Vector2(territoryImageNode.Bounds.Right + 8.0f, 0.0f);
territoryDescriptionNode.Size = territoryTitleNode.Size;
territoryDescriptionNode.Position = new Vector2(territoryTitleNode.Bounds.Left, Height / 2.0f);
}
protected override void SetNodeData(TerritoryType territory) {
if (territory.RowId is 0) return;
territoryIdNode.String = territory.RowId.ToString();
if (territory.LoadingImage.ValueNullable?.FileName is { IsEmpty: false } filePath) {
territoryImageNode.LoadTexture($"ui/loadingimage/{filePath}_hr1.tex");
territoryImageNode.IsVisible = true;
}
else {
territoryImageNode.IsVisible = false;
}
placeholderImageNode.IsVisible = !territoryImageNode.IsVisible;
territoryTitleNode.String = territory.PlaceName.ValueNullable?.Name.ToString() ?? string.Empty;
territoryDescriptionNode.String = territory.ContentFinderCondition.RowId is 0 ? string.Empty : territory.ContentFinderCondition.Value.Name.ToString();
}
}
+117
View File
@@ -0,0 +1,117 @@
using System;
using System.Numerics;
using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
namespace KamiToolKit.Premade.Nodes;
public unsafe class AlphaBarNode : SimpleComponentNode {
public readonly ImGuiImageNode AlphaBarBackgroundNode;
public readonly ImGuiImageNode AlphaBarGradientNode;
public readonly ImGuiImageNode AlphaBarSelectorNode;
private readonly ViewportEventListener alphaEventListener;
private bool isAlphaDragging;
public AlphaBarNode() {
alphaEventListener = new ViewportEventListener(AlphaSliderEvent);
AlphaBarBackgroundNode = new AlphaImageNode();
AlphaBarBackgroundNode.AttachNode(this);
AlphaBarGradientNode = new ImGuiImageNode {
TexturePath = DalamudInterface.Instance.GetAssetPath("VerticalGradient_WhiteToAlpha.png"),
FitTexture = true,
};
AlphaBarGradientNode.AttachNode(this);
AlphaBarGradientNode.AddEvent(AtkEventType.MouseDown, OnAlphaBarMouseDown);
AlphaBarSelectorNode = new ImGuiImageNode {
TexturePath = DalamudInterface.Instance.GetAssetPath("alpha_selector.png"),
FitTexture = true,
};
AlphaBarSelectorNode.AttachNode(this);
AlphaBarSelectorNode.AddEvent(AtkEventType.MouseDown, OnAlphaBarMouseDown);
}
protected override void Dispose(bool disposing, bool isNativeDestructor) {
if (disposing) {
base.Dispose(disposing, isNativeDestructor);
alphaEventListener.Dispose();
}
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
AlphaBarBackgroundNode.Size = Size;
AlphaBarGradientNode.Size = Size;
AlphaBarSelectorNode.Size = new Vector2(Width + 4.0f, 10.0f);
AlphaBarSelectorNode.Position = new Vector2(-2.0f, 0.0f);
}
private void OnAlphaBarMouseDown() {
if (!isAlphaDragging) {
alphaEventListener.AddEvent(AtkEventType.MouseMove, AlphaBarGradientNode);
alphaEventListener.AddEvent(AtkEventType.MouseUp, AlphaBarGradientNode);
isAlphaDragging = true;
}
}
private void AlphaSliderEvent(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
switch (eventType) {
case AtkEventType.MouseUp:
alphaEventListener.RemoveEvent(AtkEventType.MouseMove);
alphaEventListener.RemoveEvent(AtkEventType.MouseUp);
isAlphaDragging = false;
break;
case AtkEventType.MouseMove: {
var mousePosition = new Vector2(atkEventData->MouseData.PosX, atkEventData->MouseData.PosY);
var scale = ParentAddon is not null ? ParentAddon->Scale : 1.0f;
var scaledHeight = AlphaBarGradientNode.Height * scale;
var minY = AlphaBarGradientNode.ScreenY;
var maxY = AlphaBarGradientNode.ScreenY + scaledHeight;
if (mousePosition.Y >= minY && mousePosition.Y <= maxY) {
var alphaRatio = 1.0f - (mousePosition.Y - AlphaBarGradientNode.ScreenY) / scaledHeight;
AlphaBarSelectorNode.Y = Height - Height * alphaRatio - 5.0f;
OnAlphaChanged?.Invoke(alphaRatio);
}
else if (mousePosition.Y < minY) {
AlphaBarSelectorNode.Y = -4.0f;
OnAlphaChanged?.Invoke(1.0f);
}
else if (mousePosition.Y > maxY) {
AlphaBarSelectorNode.Y = Height - 4.0f;
OnAlphaChanged?.Invoke(0.0f);
}
break;
}
}
}
public Action<float>? OnAlphaChanged { get; init; }
public override Vector4 Color {
get => AlphaBarGradientNode.Color;
set {
AlphaBarGradientNode.MultiplyColor = value.AsVector3();
AlphaBarSelectorNode.Y = Height - Height * value.W - 5.0f;
}
}
public override ColorHelpers.HsvaColor ColorHsva {
get => AlphaBarGradientNode.MultiplyColorHsva;
set {
AlphaBarGradientNode.MultiplyColorHsva = value with { A = 1.0f };
AlphaBarSelectorNode.Y = Height - Height * value.A - 5.0f;
}
}
}
+18
View File
@@ -0,0 +1,18 @@
using System;
using KamiToolKit.Nodes;
namespace KamiToolKit.Premade.Nodes;
public abstract class ConfigNode<T> : SimpleComponentNode {
public T? ConfigurationOption {
get;
set {
field = value;
OptionChanged(value);
}
}
protected abstract void OptionChanged(T? option);
public Action<T>? OnConfigChanged { get; set; }
}
+203
View File
@@ -0,0 +1,203 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Utility;
using KamiToolKit.Nodes;
using KamiToolKit.Premade.Widgets;
namespace KamiToolKit.Premade.Nodes;
/// <summary>
/// A non-owning list node that supports searching, and various callbacks for easily editing a list.
/// </summary>
/// <typeparam name="T">Data type to display the data for.</typeparam>
/// <typeparam name="TU">ListItemNode derived type, for defining the result view.</typeparam>
public class ModifyListNode<T, TU> : SimpleComponentNode where TU : ListItemNode<T>, new() {
private readonly SearchWidget searchWidget;
private readonly ListNode<T, TU> listNode;
private readonly TextButtonNode addButton;
private readonly TextButtonNode editButton;
private readonly TextButtonNode removeButton;
public ModifyListNode() {
searchWidget = new SearchWidget {
OnSortOrderChanged = OnSortOrderChanged,
OnSearchUpdated = OnSearchUpdated,
};
searchWidget.AttachNode(this);
listNode = new ListNode<T, TU> {
OptionsList = [],
OnItemSelected = OnListItemSelected,
};
listNode.AttachNode(this);
addButton = new TextButtonNode {
String = "Add",
OnClick = OnAddClicked,
IsEnabled = false,
};
addButton.AttachNode(this);
editButton = new TextButtonNode {
String = "Edit",
OnClick = OnEditClicked,
IsEnabled = false,
};
editButton.AttachNode(this);
removeButton = new TextButtonNode {
String = "Remove",
OnClick = OnRemoveClicked,
IsEnabled = false,
};
removeButton.AttachNode(this);
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
searchWidget.Size = new Vector2(Width, 65.0f);
searchWidget.Position = Vector2.Zero;
listNode.Size = new Vector2(Width, Height - searchWidget.Height - 40.0f);
listNode.Position = new Vector2(0.0f, searchWidget.Y + searchWidget.Height + 8.0f);
const float buttonPadding = 5.0f;
var buttonWidth = (Width - buttonPadding * 2.0f) / 3.0f;
addButton.Size = new Vector2(buttonWidth, 24.0f);
addButton.Position = new Vector2(0.0f, Height - 24.0f);
editButton.Size = new Vector2(buttonWidth, 24.0f);
editButton.Position = new Vector2(buttonWidth + buttonPadding, Height - 24.0f);
removeButton.Size = new Vector2(buttonWidth, 24.0f);
removeButton.Position = new Vector2(buttonWidth * 2.0f + buttonPadding * 2.0f, Height - 24.0f);
}
public List<T> Options {
get;
set {
field = value;
listNode.OptionsList = value;
}
} = [];
public List<string>? SortOptions {
get => searchWidget.SortingOptions;
set {
searchWidget.SortingOptions = value ?? [];
OnSizeChanged();
if (value is not null && value.Count > 0) {
OnSortOrderChanged(value.First(), false);
}
}
}
public Action<T?>? SelectionChanged { get; init; }
public Action? AddNewEntry {
get;
set {
field = value;
addButton.IsEnabled = value is not null;
}
}
public Action<T>? RemoveEntry {
get;
set {
field = value;
removeButton.IsEnabled = value is not null && SelectedOption is not null;
}
}
public Action<T>? EditEntry {
get;
set {
field = value;
editButton.IsEnabled = value is not null && SelectedOption is not null;
}
}
public delegate int ItemCompareDelegate(T left, T right, string sortingMode);
public ItemCompareDelegate? ItemComparer { get; set; }
public delegate bool IsSearchMatchDelegate(T obj, string searchString);
public IsSearchMatchDelegate? IsSearchMatch { get; set; }
public T? SelectedOption { get; private set; }
public float ItemSpacing {
get => listNode.ItemSpacing;
set {
listNode.ItemSpacing = value;
OnSizeChanged();
}
}
private void OnSortOrderChanged(string sortingString, bool reversed) {
if (ItemComparer is null) return;
var listCopy = Options.ToList();
listCopy.Sort((left, right) => ItemComparer.Invoke(left, right, sortingString) * (reversed ? -1 : 1));
listNode.OptionsList = listCopy;
UpdateButtonStates();
}
private void OnSearchUpdated(string searchString) {
if (IsSearchMatch is null) return;
if (searchString.IsNullOrEmpty()) {
listNode.OptionsList = Options;
}
else {
listNode.OptionsList = Options.Where(item => IsSearchMatch(item, searchString)).ToList();
}
}
private void OnListItemSelected(T? obj) {
SelectedOption = obj;
SelectionChanged?.Invoke(SelectedOption);
UpdateButtonStates();
}
private void OnAddClicked() {
AddNewEntry?.Invoke();
RefreshList();
}
private void OnEditClicked() {
if (SelectedOption is null) return;
EditEntry?.Invoke(SelectedOption);
RefreshList();
}
private void OnRemoveClicked() {
if (SelectedOption is null) return;
RemoveEntry?.Invoke(SelectedOption);
RefreshList();
}
private void UpdateButtonStates() {
editButton.IsEnabled = SelectedOption is not null && EditEntry is not null;
removeButton.IsEnabled = SelectedOption is not null && RemoveEntry is not null;
}
/// <summary>
/// Refreshes the displayed list data.
/// This resets scroll position, so don't spam it.
/// </summary>
public void RefreshList() {
OnSortOrderChanged(searchWidget.SortMode, searchWidget.IsReversed);
OnSearchUpdated(searchWidget.SearchText);
listNode.FullRebuild();
}
}
@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using KamiToolKit.Nodes;
namespace KamiToolKit.Premade.Nodes;
/// <summary>
/// A TextButton that has a configurable set of states
/// </summary>
public class MultiStateButtonNode<T> : TextButtonNode where T : notnull {
public Action<T>? OnStateChanged { get; set; }
public MultiStateButtonNode()
=> OnClick = CycleState;
public required List<T> States {
get;
set {
field = value;
UpdateDisplay();
}
}
private int SelectedIndex {
get;
set {
field = value;
UpdateDisplay();
}
}
public T SelectedState {
get => States[SelectedIndex];
set => SelectedIndex = States.IndexOf(value);
}
private void CycleState() {
if (States.Count is 0) return;
SelectedIndex = (SelectedIndex + 1) % States.Count;
OnStateChanged?.Invoke(SelectedState);
}
private void UpdateDisplay() {
if (SelectedIndex < 0) return;
if (SelectedIndex > States.Count - 1) return;
String = GetStateText(States[SelectedIndex]);
}
protected virtual string GetStateText(T state) {
if (state is Enum enumState) {
return enumState.Description;
}
return state.ToString() ?? "Unable to Parse Type";
}
}
@@ -0,0 +1,44 @@
using System.Numerics;
using KamiToolKit.Nodes;
using Lumina.Text.ReadOnly;
namespace KamiToolKit.Premade.Nodes;
public class UnderlinedTextNode : SimpleComponentNode {
public readonly CategoryTextNode LabelTextNode;
public readonly HorizontalLineNode LineNode;
public UnderlinedTextNode() {
LabelTextNode = new CategoryTextNode();
LabelTextNode.AttachNode(this);
LineNode = new HorizontalLineNode {
Height = 4.0f,
};
LineNode.AttachNode(this);
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
LabelTextNode.Size = new Vector2(Width, Height - 4.0f);
LabelTextNode.Position = new Vector2(0.0f, 0.0f);
LineNode.Position = new Vector2(0.0f, LabelTextNode.Bounds.Bottom - 4.0f);
RecalculateLineSize();
}
public ReadOnlySeString String {
get => LabelTextNode.String;
set {
LabelTextNode.String = value;
RecalculateLineSize();
}
}
private void RecalculateLineSize() {
var textSize = LabelTextNode.GetTextDrawSize();
LineNode.Width = textSize.X + 32.0f;
}
}
@@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using FFXIVClientStructs.Interop;
using KamiToolKit.Premade.ListItemNodes;
namespace KamiToolKit.Premade.SearchAddons;
public unsafe class AddonSearchAddon : BaseSearchAddon<Pointer<AtkUnitBase>, AddonListItemNode> {
public AddonSearchAddon() {
SearchOptions = GetAllAddons();
SortingOptions = [ "Visibility", "Alphabetical" ];
ItemSpacing = 3.0f;
}
protected override int Comparer(Pointer<AtkUnitBase> left, Pointer<AtkUnitBase> right, string sortingString, bool reversed) {
if (left.Value is null || right.Value is null) return 0;
switch (sortingString) {
case "Alphabetical":
return string.CompareOrdinal(left.Value->NameString, right.Value->NameString) * (reversed ? -1 : 1);
case "Visibility":
var visibilityComparison = right.Value->IsVisible.CompareTo(left.Value->IsVisible);
if (visibilityComparison is 0) {
visibilityComparison = string.CompareOrdinal(left.Value->NameString, right.Value->NameString);
}
return visibilityComparison * (reversed ? -1 : 1);
}
return 0;
}
protected override bool IsMatch(Pointer<AtkUnitBase> item, string searchString) {
var regex = new Regex(searchString,RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
return regex.IsMatch(item.Value->NameString);
}
private static List<Pointer<AtkUnitBase>> GetAllAddons() {
List<Pointer<AtkUnitBase>> addons = [];
foreach (var entry in RaptureAtkUnitManager.Instance()->AllLoadedUnitsList.Entries) {
if (entry.Value is null) continue;
addons.Add(entry);
}
return addons;
}
}
@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Nodes;
using KamiToolKit.Premade.Widgets;
namespace KamiToolKit.Premade.SearchAddons;
public abstract class BaseSearchAddon<T, TU> : NativeAddon where TU : ListItemNode<T>, new() {
private SearchWidget? searchWidget;
private ListNode<T, TU>? listNode;
private TextButtonNode? cancelButton;
private TextButtonNode? confirmButton;
private T? selectedOption;
protected override unsafe void OnSetup(AtkUnitBase* addon) {
searchWidget = new SearchWidget {
Size = ContentSize,
Position = ContentStartPosition,
SortingOptions = SortingOptions,
OnSortOrderChanged = OnSortOrderUpdated,
OnSearchUpdated = OnSearchUpdated,
};
searchWidget.AttachNode(this);
listNode = new ListNode<T, TU> {
Position = new Vector2(ContentStartPosition.X, searchWidget.Y + searchWidget.Height + 8.0f),
Size = new Vector2(ContentSize.X, ContentSize.Y - searchWidget.Height - 16.0f - 24.0f - 8.0f),
ItemSpacing = ItemSpacing,
OptionsList = SearchOptions,
OnItemSelected = item => {
selectedOption = item;
confirmButton?.IsEnabled = true;
},
};
listNode.AttachNode(this);
const float buttonPadding = 20.0f;
var contentWidth = ContentSize.X - buttonPadding * 2;
var buttonWidth = contentWidth / 3.0f;
cancelButton = new TextButtonNode {
Size = new Vector2(buttonWidth, 24.0f),
Position = new Vector2(ContentStartPosition.X, ContentStartPosition.Y + ContentSize.Y - 24.0f - 8.0f),
String = "Cancel",
OnClick = OnCancelClicked,
};
cancelButton.AttachNode(this);
confirmButton = new TextButtonNode {
Size = new Vector2(buttonWidth, 24.0f),
Position = new Vector2(ContentStartPosition.X + buttonWidth * 2 + buttonPadding * 2, ContentStartPosition.Y + ContentSize.Y - 24.0f - 8.0f),
IsEnabled = false,
String = "Confirm",
OnClick = OnConfirmClicked,
};
confirmButton.AttachNode(this);
if (SortingOptions.Count > 0) {
OnSortOrderUpdated(SortingOptions.First(), false);
}
}
private void OnCancelClicked() {
selectedOption = default;
Close();
}
private void OnConfirmClicked() {
if (selectedOption is not null) {
SelectionResult?.Invoke(selectedOption);
}
selectedOption = default;
Close();
}
private void OnSortOrderUpdated(string sortingString, bool reversed) {
var resortedList = SearchOptions.ToList();
resortedList.Sort((left, right) => Comparer(left, right, sortingString, reversed));
listNode?.OptionsList = resortedList;
}
private void OnSearchUpdated(string searchString) {
listNode?.OptionsList = SearchOptions.Where(item => IsMatch(item, searchString)).ToList();
}
protected abstract int Comparer(T left, T right, string sortingString, bool reversed);
protected abstract bool IsMatch(T item, string searchString);
public List<string> SortingOptions { get; init; } = [ "Alphabetical", "Id" ];
public List<T> SearchOptions {
get;
set {
field = value;
listNode?.OptionsList = value;
}
} = [];
public float ItemSpacing {
get;
set {
field = value;
listNode?.ItemSpacing = value;
}
} = 6.0f;
public Action<T>? SelectionResult { get; set; }
}
@@ -0,0 +1,28 @@
using System.Collections.Generic;
using System.Linq;
using KamiToolKit.Classes;
using KamiToolKit.Premade.ListItemNodes;
using Lumina.Excel.Sheets;
namespace KamiToolKit.Premade.SearchAddons;
public class CurrencySearchAddon : ItemSearchAddonBase<CurrencyListItemNode> {
public CurrencySearchAddon()
=> SearchOptions = GetCurrencyItems().ToList();
private static IEnumerable<Item> GetCurrencyItems() {
var dataManager = DalamudInterface.Instance.DataManager;
var obsoleteTomes = dataManager.GetExcelSheet<TomestonesItem>()
.Where(item => item.Tomestones.RowId is 0)
.Select(item => item.Item.Value)
.ToHashSet(EqualityComparer<Item>.Create(
(x, y) => x.RowId == y.RowId,
obj => obj.RowId.GetHashCode()
));
return dataManager.GetExcelSheet<Item>()
.Where(item => item is { Name.IsEmpty: false, ItemUICategory.RowId: 100 } or { RowId: >= 1 and < 100, Name.IsEmpty: false })
.Where(item => !obsoleteTomes.Contains(item));
}
}
@@ -0,0 +1,5 @@
using KamiToolKit.Premade.ListItemNodes;
namespace KamiToolKit.Premade.SearchAddons;
public class ItemSearchAddon : ItemSearchAddonBase<ItemListItemNode>;
@@ -0,0 +1,37 @@
using System.Text.RegularExpressions;
using KamiToolKit.Nodes;
using Lumina.Excel.Sheets;
namespace KamiToolKit.Premade.SearchAddons;
public class ItemSearchAddonBase<T> : BaseSearchAddon<Item, T> where T : ListItemNode<Item>, new() {
protected override int Comparer(Item left, Item right, string sortingString, bool reversed) {
var result = sortingString switch {
"Alphabetical" => string.CompareOrdinal(left.Name.ToString(), right.Name.ToString()),
"Id" => left.RowId.CompareTo(right.RowId),
_ => 0,
};
return reversed ? -result : result;
}
protected override bool IsMatch(Item item, string searchString) {
var isDescriptionSearch = searchString.StartsWith('$');
if (isDescriptionSearch) {
searchString = searchString[1..];
}
var regex = new Regex(searchString,RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
if (regex.IsMatch(item.RowId.ToString())) return true;
if (regex.IsMatch(item.Name.ToString())) return true;
if (regex.IsMatch(item.Description.ToString()) && isDescriptionSearch) return true;
if (regex.IsMatch(item.LevelEquip.ToString())) return true;
if (regex.IsMatch(item.LevelItem.RowId.ToString())) return true;
if (regex.IsMatch(item.ClassJobCategory.Value.Name.ToString())) return true;
if (regex.IsMatch(item.ItemUICategory.Value.Name.ToString())) return true;
return false;
}
}
@@ -0,0 +1,35 @@
using System.Linq;
using System.Text.RegularExpressions;
using KamiToolKit.Classes;
using KamiToolKit.Premade.ListItemNodes;
using Lumina.Excel.Sheets;
namespace KamiToolKit.Premade.SearchAddons;
public class StatusSearchAddon : BaseSearchAddon<Status, StatusListItemNode> {
public StatusSearchAddon() {
SearchOptions = DalamudInterface.Instance.DataManager.GetExcelSheet<Status>()
.Where(territory => territory.RowId is not 0)
.Where(territory => !territory.Name.IsEmpty)
.ToList();
}
protected override int Comparer(Status left, Status right, string sortingString, bool reversed){
var result = sortingString switch {
"Alphabetical" => string.CompareOrdinal(left.Name.ToString(), right.Name.ToString()),
"Id" => left.RowId.CompareTo(right.RowId),
_ => 0,
};
return reversed ? -result : result;
}
protected override bool IsMatch(Status item, string searchString) {
var regex = new Regex(searchString,RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
if (regex.IsMatch(item.RowId.ToString())) return true;
if (regex.IsMatch(item.Name.ToString())) return true;
return false;
}
}
@@ -0,0 +1,38 @@
using System.Linq;
using System.Text.RegularExpressions;
using Dalamud.Utility;
using KamiToolKit.Classes;
using KamiToolKit.Premade.ListItemNodes;
using Lumina.Excel.Sheets;
namespace KamiToolKit.Premade.SearchAddons;
public class TerritorySearchAddon : BaseSearchAddon<TerritoryType, TerritoryTypeListItemNode> {
public TerritorySearchAddon() {
SearchOptions = DalamudInterface.Instance.DataManager.GetExcelSheet<TerritoryType>()
.Where(territory => territory.RowId is not 0)
.Where(territory => territory.LoadingImage.RowId is not 0)
.Where(territory => !territory.PlaceName.ValueNullable?.Name.ToString().IsNullOrEmpty() ?? false)
.ToList();
}
protected override int Comparer(TerritoryType left, TerritoryType right, string sortingString, bool reversed) {
var result = sortingString switch {
"Alphabetical" => string.CompareOrdinal(left.Name.ToString(), right.Name.ToString()),
"Id" => left.RowId.CompareTo(right.RowId),
_ => 0,
};
return reversed ? -result : result;
}
protected override bool IsMatch(TerritoryType item, string searchString) {
var regex = new Regex(searchString,RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
if (regex.IsMatch(item.RowId.ToString())) return true;
if (regex.IsMatch(item.PlaceName.ValueNullable?.Name.ToString() ?? string.Empty)) return true;
if (regex.IsMatch(item.ContentFinderCondition.ValueNullable?.Name.ToString() ?? string.Empty)) return true;
return false;
}
}
+109
View File
@@ -0,0 +1,109 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using KamiToolKit.Nodes;
using Lumina.Text.ReadOnly;
namespace KamiToolKit.Premade.Widgets;
/// <summary>
/// Represents a search element that has a searchbar, and a dropdown for reordering elements.
/// </summary>
public unsafe class SearchWidget : SimpleComponentNode {
public readonly TextInputNode InputNode;
public readonly TextDropDownNode SortOrderDropDown;
public readonly CircleButtonNode ReverseButtonNode;
public bool IsReversed { get; private set; }
public string SearchText { get; private set; } = string.Empty;
public string SortMode { get; private set; } = string.Empty;
public delegate void SearchUpdated(string searchString);
public delegate void SortUpdated(string sortingString, bool reversed);
public SearchWidget() {
InputNode = new TextInputNode {
PlaceholderString = "Search . . .",
String = SearchText,
OnInputReceived = SearchTextChanged,
};
InputNode.AttachNode(this);
SortOrderDropDown = new TextDropDownNode {
MaxListOptions = 0,
Options = [],
IsVisible = false,
SelectedOption = SortMode == string.Empty ? null : SortMode,
OnOptionSelected = DropDownChanged,
};
SortOrderDropDown.AttachNode(this);
ReverseButtonNode = new CircleButtonNode {
Icon = ButtonIcon.Sort,
OnClick = OnReverseButtonClicked,
TextTooltip = "Reverse Sort Direction",
IsVisible = false,
};
ReverseButtonNode.AttachNode(this);
ResNode->SetHeight(38);
}
public required SortUpdated OnSortOrderChanged { get; set; }
private void OnReverseButtonClicked() {
IsReversed = !IsReversed;
OnSortOrderChanged(SortMode, IsReversed);
}
private void DropDownChanged(string newOption) {
SortMode = newOption;
OnSortOrderChanged(SortMode, IsReversed);
}
public required SearchUpdated OnSearchUpdated { get; set; }
private void SearchTextChanged(ReadOnlySeString newSearchString) {
SearchText = newSearchString.ToString();
OnSearchUpdated(SearchText);
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
InputNode.Size = new Vector2(Width - 10.0f, 28.0f);
InputNode.Position = new Vector2(5.0f, 5.0f);
ReverseButtonNode.Size = new Vector2(28.0f, 28.0f);
ReverseButtonNode.Position = new Vector2(Width - 5.0f - ReverseButtonNode.Width, InputNode.Height + 8.0f);
SortOrderDropDown.Size = new Vector2(Width - 5.0f - ReverseButtonNode.Width - 5.0f - 5.0f, 28.0f);
SortOrderDropDown.Position = new Vector2(5.0f, InputNode.Height + 8.0f);
}
// Disallow modifying the height of this element.
public override float Height { get => base.Height; set { } }
public int MaxDropdownOptions {
get => SortOrderDropDown.MaxListOptions;
set => SortOrderDropDown.MaxListOptions = value;
}
public List<string> SortingOptions {
get => SortOrderDropDown.Options ?? [];
set {
SortOrderDropDown.Options = value;
SortOrderDropDown.MaxListOptions = value.Count / 2 + 1;
SortOrderDropDown.IsVisible = value.Count > 0;
ReverseButtonNode.IsVisible = value.Count > 0;
ResNode->SetHeight((ushort)(value.Count > 0 ? 69 : 38));
if (SortingOptions.Count > 0) {
SortMode = value.First();
}
}
}
public string? SelectedOption => SortOrderDropDown.SelectedOption;
}
@@ -0,0 +1,94 @@
using System;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
namespace KamiToolKit.Premade.Widgets;
public class Vector2EditWidget : SimpleComponentNode {
public readonly GridNode GridNode;
public readonly TextNode WidthTextNode;
public readonly TextNode HeightTextNode;
public readonly NumericInputNode WidthInputNode;
public readonly NumericInputNode HeightInputNode;
public Vector2EditWidget() {
GridNode = new GridNode {
GridSize = new GridSize(2, 2),
};
GridNode.AttachNode(this);
WidthTextNode = new TextNode {
AlignmentType = AlignmentType.Bottom,
FontType = FontType.Axis,
FontSize = 14,
LineSpacing = 14,
TextColor = ColorHelper.GetColor(8),
TextOutlineColor = ColorHelper.GetColor(7),
TextFlags = TextFlags.Edge | TextFlags.AutoAdjustNodeSize,
String = XLabel ?? "Width",
};
WidthTextNode.AttachNode(GridNode[0, 0]);
HeightTextNode = new TextNode {
AlignmentType = AlignmentType.Bottom,
FontType = FontType.Axis,
FontSize = 14,
LineSpacing = 14,
TextColor = ColorHelper.GetColor(8),
TextOutlineColor = ColorHelper.GetColor(7),
TextFlags = TextFlags.Edge | TextFlags.AutoAdjustNodeSize,
String = YLabel ?? "Height",
};
HeightTextNode.AttachNode(GridNode[1, 0]);
WidthInputNode = new NumericInputNode {
Position = new Vector2(2.0f, 2.0f),
OnValueUpdate = OnXValueUpdated,
};
WidthInputNode.AttachNode(GridNode[0, 1]);
HeightInputNode = new NumericInputNode {
Position = new Vector2(2.0f, 2.0f),
OnValueUpdate = OnYValueUpdated,
};
HeightInputNode.AttachNode(GridNode[1, 1]);
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
GridNode.Size = Size;
WidthTextNode.Size = GridNode[0, 0].Size;
HeightTextNode.Size = GridNode[1, 0].Size;
WidthInputNode.Size = GridNode[0, 1].Size;
HeightInputNode.Size = GridNode[1, 1].Size;
}
private void OnXValueUpdated(int newValue) {
Value = Value with { X = newValue };
OnValueChanged?.Invoke(Value);
}
private void OnYValueUpdated(int newValue) {
Value = Value with { Y = newValue };
OnValueChanged?.Invoke(Value);
}
public Vector2 Value {
get;
set {
field = value;
WidthInputNode.Value = (int) value.X;
HeightInputNode.Value = (int) value.Y;
}
}
public Action<Vector2>? OnValueChanged { get; set; }
public string? XLabel { get; set; }
public string? YLabel { get; set; }
}