Initial commit: AetherBags + KamiToolKit for FC Gitea
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Input;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using InteropGenerator.Runtime;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Enums;
|
||||
using KamiToolKit.Timelines;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
namespace KamiToolKit.Nodes;
|
||||
|
||||
public unsafe class TextInputNode : ComponentNode<AtkComponentTextInput, AtkUldComponentDataTextInput> {
|
||||
public readonly NineGridNode BackgroundNode;
|
||||
public readonly TextNode CurrentTextNode;
|
||||
public readonly CursorNode CursorNode;
|
||||
public readonly NineGridNode FocusNode;
|
||||
public readonly TextInputSelectionListNode SelectionListNode;
|
||||
public readonly TextNode TextLimitsNode;
|
||||
public readonly TextNode PlaceholderTextNode;
|
||||
|
||||
private AtkComponentInputBase.CallbackDelegate? pinnedCallbackFunction;
|
||||
|
||||
public TextInputNode() {
|
||||
SetInternalComponentType(ComponentType.TextInput);
|
||||
|
||||
BackgroundNode = new SimpleNineGridNode {
|
||||
NodeId = 19,
|
||||
TexturePath = "ui/uld/TextInputA.tex",
|
||||
TextureCoordinates = new Vector2(24.0f, 0.0f),
|
||||
TextureSize = new Vector2(24.0f, 24.0f),
|
||||
Offsets = new Vector4(10.0f),
|
||||
Size = new Vector2(152.0f, 28.0f),
|
||||
};
|
||||
BackgroundNode.AttachNode(this);
|
||||
|
||||
FocusNode = new SimpleNineGridNode {
|
||||
NodeId = 18,
|
||||
TexturePath = "ui/uld/TextInputA.tex",
|
||||
TextureCoordinates = new Vector2(0.0f, 0.0f),
|
||||
TextureSize = new Vector2(24.0f, 24.0f),
|
||||
Offsets = new Vector4(10.0f),
|
||||
Size = new Vector2(152.0f, 28.0f),
|
||||
};
|
||||
FocusNode.AttachNode(this);
|
||||
|
||||
TextLimitsNode = new TextNode {
|
||||
NodeId = 17,
|
||||
Position = new Vector2(-24.0f, 6.0f),
|
||||
Size = new Vector2(170.0f, 19.0f),
|
||||
FontType = FontType.MiedingerMed,
|
||||
FontSize = 14,
|
||||
AlignmentType = (AlignmentType)21,
|
||||
};
|
||||
TextLimitsNode.AttachNode(this);
|
||||
|
||||
CurrentTextNode = new TextNode {
|
||||
NodeId = 16,
|
||||
Position = new Vector2(10.0f, 6.0f),
|
||||
Size = new Vector2(132.0f, 18.0f),
|
||||
AlignmentType = AlignmentType.TopLeft,
|
||||
TextFlags = TextFlags.AutoAdjustNodeSize,
|
||||
TextColor = ColorHelper.GetColor(1),
|
||||
};
|
||||
CurrentTextNode.AttachNode(this);
|
||||
|
||||
SelectionListNode = new TextInputSelectionListNode {
|
||||
NodeId = 4,
|
||||
Position = new Vector2(0.0f, 22.0f),
|
||||
Size = new Vector2(186.0f, 208.0f),
|
||||
};
|
||||
SelectionListNode.AttachNode(this);
|
||||
|
||||
CursorNode = new CursorNode {
|
||||
NodeId = 2,
|
||||
Position = new Vector2(10.0f, 2.0f),
|
||||
Size = new Vector2(4.0f, 24.0f),
|
||||
OriginY = 4.0f,
|
||||
};
|
||||
CursorNode.AttachNode(this);
|
||||
|
||||
PlaceholderTextNode = new TextNode {
|
||||
Position = new Vector2(8.0f, 0.0f),
|
||||
TextColor = ColorHelper.GetColor(3),
|
||||
};
|
||||
PlaceholderTextNode.AttachNode(this);
|
||||
|
||||
Data->Nodes[0] = CurrentTextNode.NodeId;
|
||||
Data->Nodes[1] = BackgroundNode.NodeId;
|
||||
Data->Nodes[2] = CursorNode.NodeId;
|
||||
Data->Nodes[3] = SelectionListNode.NodeId;
|
||||
Data->Nodes[4] = SelectionListNode.Buttons[8].NodeId;
|
||||
Data->Nodes[5] = SelectionListNode.Buttons[7].NodeId;
|
||||
Data->Nodes[6] = SelectionListNode.Buttons[6].NodeId;
|
||||
Data->Nodes[7] = SelectionListNode.Buttons[5].NodeId;
|
||||
Data->Nodes[8] = SelectionListNode.Buttons[4].NodeId;
|
||||
Data->Nodes[9] = SelectionListNode.Buttons[3].NodeId;
|
||||
Data->Nodes[10] = SelectionListNode.Buttons[2].NodeId;
|
||||
Data->Nodes[11] = SelectionListNode.Buttons[1].NodeId;
|
||||
Data->Nodes[12] = SelectionListNode.Buttons[0].NodeId;
|
||||
Data->Nodes[13] = SelectionListNode.LabelNode.NodeId;
|
||||
Data->Nodes[14] = SelectionListNode.BackgroundNode.NodeId;
|
||||
Data->Nodes[15] = TextLimitsNode.NodeId;
|
||||
|
||||
Data->CandidateColor = new ByteColor { R = 66 };
|
||||
Data->IMEColor = new ByteColor { R = 67 };
|
||||
Data->FocusColor = KnownColor.Black.Vector().ToByteColor();
|
||||
|
||||
Flags = TextInputFlags.EnableIme | TextInputFlags.AllowUpperCase | TextInputFlags.AllowLowerCase |
|
||||
TextInputFlags.EnableDictionary | TextInputFlags.AllowNumberInput | TextInputFlags.AllowSymbolInput;
|
||||
|
||||
EnableCompletion = false;
|
||||
Component->EnableTabCallback = true;
|
||||
|
||||
LoadTimelines();
|
||||
|
||||
pinnedCallbackFunction = OnCallback;
|
||||
Component->Callback = (delegate* unmanaged<AtkUnitBase*, InputCallbackType, CStringPointer, CStringPointer, int, InputCallbackResult>) Marshal.GetFunctionPointerForDelegate(pinnedCallbackFunction);
|
||||
|
||||
InitializeComponentEvents();
|
||||
|
||||
CollisionNode.AddEvent(AtkEventType.FocusStart, () => {
|
||||
PlaceholderTextNode.IsVisible = false;
|
||||
OnFocused?.Invoke();
|
||||
|
||||
if (AutoSelectAll && Component->EvaluatedString.Length > 0) {
|
||||
DalamudInterface.Instance.Framework.RunOnTick(() => {
|
||||
var keyModifiers = new AtkTextInput.KeyModifiers {
|
||||
IsControlDown = true,
|
||||
};
|
||||
|
||||
AtkStage.Instance()->AtkInputManager->TextInput->ProcessKeyShortcut(SeVirtualKey.A, &keyModifiers);
|
||||
}, delayTicks: 1);
|
||||
}
|
||||
});
|
||||
|
||||
CollisionNode.AddEvent(AtkEventType.FocusStop, () => {
|
||||
OnUnfocused?.Invoke();
|
||||
if (!PlaceholderString.IsNullOrEmpty() && String.IsEmpty) {
|
||||
PlaceholderTextNode.IsVisible = true;
|
||||
PlaceholderTextNode.String = PlaceholderString;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing, bool isNativeDestructor) {
|
||||
if (disposing) {
|
||||
base.Dispose(disposing, isNativeDestructor);
|
||||
|
||||
pinnedCallbackFunction = null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsFocused
|
||||
=> AtkStage.Instance()->AtkInputManager->FocusedNode == CollisionNode.Node;
|
||||
|
||||
public int MaxCharacters {
|
||||
get => (int)Component->ComponentTextData.MaxChar;
|
||||
set => Component->ComponentTextData.MaxChar = (uint)value;
|
||||
}
|
||||
|
||||
public bool ShowLimitText {
|
||||
get => TextLimitsNode.IsVisible;
|
||||
set => TextLimitsNode.IsVisible = value;
|
||||
}
|
||||
|
||||
public TextInputFlags Flags {
|
||||
get => (TextInputFlags) ((byte)Data->Flags1 | (byte)Data->Flags2 << 8);
|
||||
set {
|
||||
Data->Flags1 = (TextInputFlags1)((ushort)value & 0xFF);
|
||||
Data->Flags2 = (TextInputFlags2)((ushort)value >> 8);
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnableCompletion {
|
||||
get => Component->EnableCompletion;
|
||||
set => Component->EnableCompletion = value;
|
||||
}
|
||||
|
||||
public bool EnableFocusSounds {
|
||||
get => Component->EnableFocusSounds;
|
||||
set => Component->EnableFocusSounds = value;
|
||||
}
|
||||
|
||||
public virtual ReadOnlySeString String {
|
||||
get => Component->EvaluatedString.AsSpan();
|
||||
set {
|
||||
Component->SetText(value);
|
||||
UpdatePlaceholderVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
public string? PlaceholderString {
|
||||
get;
|
||||
set {
|
||||
field = value;
|
||||
UpdatePlaceholderVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsError {
|
||||
get => FocusNode.MultiplyColor == new Vector3(1.0f, 0.6f, 0.6f);
|
||||
set => FocusNode.MultiplyColor = value ? new Vector3(1.0f, 0.6f, 0.6f) : Vector3.One;
|
||||
}
|
||||
|
||||
public bool AutoSelectAll { get; set; } = true;
|
||||
|
||||
public void ClearFocus() {
|
||||
if (IsFocused) {
|
||||
AtkStage.Instance()->AtkInputManager->SetFocus(null, ParentAddon, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual Action<ReadOnlySeString>? OnInputReceived { get; set; }
|
||||
public virtual Action<ReadOnlySeString>? OnInputComplete { get; set; }
|
||||
public Action? OnFocusLost { get; set; }
|
||||
public Action? OnEscapeEntered { get; set; }
|
||||
public Action? OnTabEntered { get; set; }
|
||||
public Action? OnFocused { get; set; }
|
||||
public Action? OnUnfocused { get; set; }
|
||||
|
||||
private InputCallbackResult OnCallback(AtkUnitBase* addon, InputCallbackType type, CStringPointer rawString, CStringPointer evaluatedString, int eventKind) {
|
||||
try {
|
||||
switch (type) {
|
||||
case InputCallbackType.Enter:
|
||||
if (this is TextMultiLineInputNode) break;
|
||||
|
||||
OnInputComplete?.Invoke(Component->EvaluatedString.AsSpan());
|
||||
ClearFocus();
|
||||
break;
|
||||
|
||||
case InputCallbackType.TextChanged:
|
||||
OnInputReceived?.Invoke(Component->EvaluatedString.AsSpan());
|
||||
break;
|
||||
|
||||
case InputCallbackType.Escape:
|
||||
OnEscapeEntered?.Invoke();
|
||||
break;
|
||||
|
||||
case InputCallbackType.FocusLost:
|
||||
OnFocusLost?.Invoke();
|
||||
break;
|
||||
|
||||
case InputCallbackType.Tab:
|
||||
OnTabEntered?.Invoke();
|
||||
break;
|
||||
}
|
||||
|
||||
return InputCallbackResult.None;
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.Exception(e);
|
||||
return InputCallbackResult.None;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePlaceholderVisibility() {
|
||||
PlaceholderTextNode.String = PlaceholderString ?? string.Empty;
|
||||
PlaceholderTextNode.IsVisible = String.IsEmpty && !PlaceholderString.IsNullOrEmpty();
|
||||
}
|
||||
|
||||
protected override void OnSizeChanged() {
|
||||
base.OnSizeChanged();
|
||||
|
||||
BackgroundNode.Size = Size;
|
||||
FocusNode.Size = Size;
|
||||
PlaceholderTextNode.Size = Size;
|
||||
TextLimitsNode.Size = new Vector2(Width + 18.0f, Height - 9.0f);
|
||||
CurrentTextNode.Size = new Vector2(Width - 20.0f, Height - 10.0f);
|
||||
}
|
||||
|
||||
private void LoadTimelines() {
|
||||
AddTimeline(new TimelineBuilder()
|
||||
.BeginFrameSet(1, 29)
|
||||
.AddLabelPair(1, 9, 17)
|
||||
.AddLabelPair(10, 19, 18)
|
||||
.AddLabelPair(20, 29, 7)
|
||||
.EndFrameSet()
|
||||
.Build());
|
||||
|
||||
BackgroundNode.AddTimeline(new TimelineBuilder()
|
||||
.AddFrameSetWithFrame(1, 9, 1, alpha: 255)
|
||||
.BeginFrameSet(10, 19)
|
||||
.AddFrame(10, alpha: 255)
|
||||
.AddFrame(12, alpha: 255)
|
||||
.EndFrameSet()
|
||||
.AddFrameSetWithFrame(20, 29, 20, alpha: 127)
|
||||
.Build());
|
||||
|
||||
FocusNode.AddTimeline(new TimelineBuilder()
|
||||
.BeginFrameSet(10, 19)
|
||||
.AddFrame(10, alpha: 0)
|
||||
.AddFrame(12, alpha: 255)
|
||||
.EndFrameSet()
|
||||
.Build());
|
||||
|
||||
TextLimitsNode.AddTimeline(new TimelineBuilder()
|
||||
.AddFrameSetWithFrame(1, 9, 1, alpha: 102)
|
||||
.BeginFrameSet(10, 19)
|
||||
.AddFrame(10, alpha: 102)
|
||||
.AddFrame(12, alpha: 127)
|
||||
.EndFrameSet()
|
||||
.AddFrameSetWithFrame(20, 29, 20, alpha: 76)
|
||||
.Build());
|
||||
|
||||
CursorNode.AddTimeline(new TimelineBuilder()
|
||||
.BeginFrameSet(1, 15)
|
||||
.AddLabel(1, 101, AtkTimelineJumpBehavior.Start, 0)
|
||||
.AddLabel(15, 0, AtkTimelineJumpBehavior.LoopForever, 101)
|
||||
.EndFrameSet()
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user