Add basic search
This commit is contained in:
@@ -7,17 +7,18 @@ using AetherBags.Inventory;
|
|||||||
using AetherBags.Nodes;
|
using AetherBags.Nodes;
|
||||||
using Dalamud.Game.Addon.Lifecycle;
|
using Dalamud.Game.Addon.Lifecycle;
|
||||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
using KamiToolKit;
|
using KamiToolKit;
|
||||||
using KamiToolKit.Classes;
|
using KamiToolKit.Classes;
|
||||||
|
using Lumina.Data.Parsing.Uld;
|
||||||
|
|
||||||
namespace AetherBags.Addons;
|
namespace AetherBags.Addons;
|
||||||
|
|
||||||
public class AddonInventoryWindow : NativeAddon
|
public class AddonInventoryWindow : NativeAddon
|
||||||
{
|
{
|
||||||
private WrappingGridNode<InventoryCategoryNode> _categoriesNode;
|
private WrappingGridNode<InventoryCategoryNode> _categoriesNode = null!;
|
||||||
|
private TextInputWithHintNode _searchInputNode = null!;
|
||||||
|
|
||||||
// Window constraints
|
// Window constraints
|
||||||
private const float MinWindowWidth = 300;
|
private const float MinWindowWidth = 300;
|
||||||
@@ -26,14 +27,12 @@ public class AddonInventoryWindow : NativeAddon
|
|||||||
private const float MaxWindowHeight = 1000;
|
private const float MaxWindowHeight = 1000;
|
||||||
|
|
||||||
// Layout settings
|
// Layout settings
|
||||||
private const float CategorySpacing = 10;
|
private const float CategorySpacing = 12;
|
||||||
private const float ItemSize = 40;
|
private const float ItemSize = 40;
|
||||||
private const float ItemPadding = 6;
|
private const float ItemPadding = 6;
|
||||||
|
|
||||||
protected override unsafe void OnSetup(AtkUnitBase* addon)
|
protected override unsafe void OnSetup(AtkUnitBase* addon)
|
||||||
{
|
{
|
||||||
Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "Inventory", OnInventoryUpdate);
|
|
||||||
addon->SubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
|
|
||||||
_categoriesNode = new WrappingGridNode<InventoryCategoryNode>
|
_categoriesNode = new WrappingGridNode<InventoryCategoryNode>
|
||||||
{
|
{
|
||||||
Position = ContentStartPosition,
|
Position = ContentStartPosition,
|
||||||
@@ -43,6 +42,20 @@ public class AddonInventoryWindow : NativeAddon
|
|||||||
};
|
};
|
||||||
_categoriesNode.AttachNode(this);
|
_categoriesNode.AttachNode(this);
|
||||||
|
|
||||||
|
var size = new Vector2(addon->Size.X / 2.0f, 28.0f);
|
||||||
|
|
||||||
|
Vector2 headerSize = new Vector2(addon->WindowHeaderCollisionNode->Width, addon->WindowHeaderCollisionNode->Height);
|
||||||
|
_searchInputNode = new TextInputWithHintNode {
|
||||||
|
Position = headerSize / 2.0f - size / 2.0f + new Vector2(25.0f, 10.0f),
|
||||||
|
Size = size,
|
||||||
|
OnInputReceived = _ => RefreshCategories(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
_searchInputNode.AttachNode(this);
|
||||||
|
|
||||||
|
Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "Inventory", OnInventoryUpdate);
|
||||||
|
addon->SubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
|
||||||
|
|
||||||
RefreshCategories();
|
RefreshCategories();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,9 +73,9 @@ public class AddonInventoryWindow : NativeAddon
|
|||||||
RefreshCategories();
|
RefreshCategories();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RefreshCategories()
|
private void RefreshCategories(bool autosize = true)
|
||||||
{
|
{
|
||||||
var categories = InventoryState.GetInventoryItemCategories();
|
var categories = InventoryState.GetInventoryItemCategories(_searchInputNode.SearchString.ExtractText());
|
||||||
|
|
||||||
float maxContentWidth = MaxWindowWidth - (ContentStartPosition.X * 2);
|
float maxContentWidth = MaxWindowWidth - (ContentStartPosition.X * 2);
|
||||||
int maxItemsPerLine = CalculateOptimalItemsPerLine(maxContentWidth);
|
int maxItemsPerLine = CalculateOptimalItemsPerLine(maxContentWidth);
|
||||||
@@ -70,39 +83,24 @@ public class AddonInventoryWindow : NativeAddon
|
|||||||
_categoriesNode.SyncWithListData(
|
_categoriesNode.SyncWithListData(
|
||||||
categories,
|
categories,
|
||||||
node => node.CategorizedInventory,
|
node => node.CategorizedInventory,
|
||||||
data =>
|
data => new InventoryCategoryNode
|
||||||
{
|
|
||||||
var node = new InventoryCategoryNode
|
|
||||||
{
|
{
|
||||||
Size = ContentSize with { Y = 120 },
|
Size = ContentSize with { Y = 120 },
|
||||||
CategorizedInventory = data
|
CategorizedInventory = data,
|
||||||
};
|
ItemsPerLine = Math.Min(data.Items.Count, maxItemsPerLine)
|
||||||
|
|
||||||
UpdateItemsPerLine(node, maxItemsPerLine);
|
|
||||||
return node;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
foreach (InventoryCategoryNode node in _categoriesNode.GetNodes<InventoryCategoryNode>())
|
foreach (InventoryCategoryNode node in _categoriesNode.GetNodes<InventoryCategoryNode>())
|
||||||
{
|
{
|
||||||
UpdateItemsPerLine(node, maxItemsPerLine);
|
node.ItemsPerLine = Math.Min(node.CategorizedInventory.Items.Count, maxItemsPerLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
AutoSizeWindow();
|
if(autosize) AutoSizeWindow();
|
||||||
}
|
|
||||||
|
|
||||||
private static void UpdateItemsPerLine(InventoryCategoryNode node, int maxItemsPerLine)
|
|
||||||
{
|
|
||||||
int itemCount = node.CategorizedInventory.Items.Count;
|
|
||||||
int itemsPerLine = Math.Min(itemCount, maxItemsPerLine);
|
|
||||||
node.SetItemsPerLine(itemsPerLine);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int CalculateOptimalItemsPerLine(float availableWidth)
|
private int CalculateOptimalItemsPerLine(float availableWidth)
|
||||||
{
|
{
|
||||||
float itemWithPadding = ItemSize + ItemPadding;
|
return Math.Clamp((int)Math.Floor((availableWidth + ItemPadding) / (ItemSize + ItemPadding)), 1, 15);
|
||||||
int maxItems = (int)Math.Floor((availableWidth + ItemPadding) / itemWithPadding);
|
|
||||||
|
|
||||||
return Math.Clamp(maxItems, 1, 15);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AutoSizeWindow()
|
private void AutoSizeWindow()
|
||||||
|
|||||||
@@ -35,9 +35,11 @@ public static unsafe class InventoryState
|
|||||||
public static bool Contains(this List<InventoryType> inventoryTypes, GameInventoryType type)
|
public static bool Contains(this List<InventoryType> inventoryTypes, GameInventoryType type)
|
||||||
=> inventoryTypes.Contains((InventoryType)type);
|
=> inventoryTypes.Contains((InventoryType)type);
|
||||||
|
|
||||||
public static List<CategorizedInventory> GetInventoryItemCategories()
|
public static List<CategorizedInventory> GetInventoryItemCategories(string filterString = "", bool invert = false)
|
||||||
{
|
{
|
||||||
var items = GetInventoryItems();
|
var items = string.IsNullOrEmpty(filterString)
|
||||||
|
? GetInventoryItems()
|
||||||
|
: GetInventoryItems(filterString, invert);
|
||||||
|
|
||||||
return items
|
return items
|
||||||
.GroupBy(GetItemUiCategoryKey)
|
.GroupBy(GetItemUiCategoryKey)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class InventoryCategoryNode : SimpleComponentNode
|
|||||||
private const float HeaderHeight = 16;
|
private const float HeaderHeight = 16;
|
||||||
private const float MinWidth = 40;
|
private const float MinWidth = 40;
|
||||||
|
|
||||||
private float? _fixedWidth = null;
|
private float? _fixedWidth;
|
||||||
|
|
||||||
public InventoryCategoryNode()
|
public InventoryCategoryNode()
|
||||||
{
|
{
|
||||||
@@ -61,17 +61,25 @@ public class InventoryCategoryNode : SimpleComponentNode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetItemsPerLine(int itemsPerLine)
|
public int ItemsPerLine
|
||||||
{
|
{
|
||||||
_itemGridNode.ItemsPerLine = itemsPerLine;
|
get => _itemGridNode.ItemsPerLine;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_itemGridNode.ItemsPerLine = value;
|
||||||
RecalculateSize();
|
RecalculateSize();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void SetFixedWidth(float width)
|
public float? FixedWidth
|
||||||
{
|
{
|
||||||
_fixedWidth = width;
|
get => _fixedWidth;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_fixedWidth = value;
|
||||||
RecalculateSize();
|
RecalculateSize();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void RecalculateSize()
|
private void RecalculateSize()
|
||||||
{
|
{
|
||||||
@@ -106,10 +114,9 @@ public class InventoryCategoryNode : SimpleComponentNode
|
|||||||
_categoryNameTextNode.Size = _categoryNameTextNode.Size with { X = calculatedWidth };
|
_categoryNameTextNode.Size = _categoryNameTextNode.Size with { X = calculatedWidth };
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool UpdateItemGrid()
|
private void UpdateItemGrid()
|
||||||
{
|
{
|
||||||
var listUpdated = _itemGridNode.SyncWithListData(CategorizedInventory.Items, node => node.ItemInfo, CreateInventoryDragDropNode);
|
_itemGridNode.SyncWithListData(CategorizedInventory.Items, node => node.ItemInfo, CreateInventoryDragDropNode);
|
||||||
return listUpdated;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data)
|
private InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data)
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using System;
|
||||||
|
using System.Numerics;
|
||||||
|
using KamiToolKit.Nodes;
|
||||||
|
using Lumina.Text;
|
||||||
|
using Lumina.Text.ReadOnly;
|
||||||
|
|
||||||
|
namespace AetherBags.Nodes;
|
||||||
|
|
||||||
|
public class TextInputWithHintNode : SimpleComponentNode {
|
||||||
|
private readonly TextInputNode textInputNode;
|
||||||
|
private readonly ImageNode helpNode;
|
||||||
|
|
||||||
|
public TextInputWithHintNode() {
|
||||||
|
textInputNode = new TextInputNode {
|
||||||
|
PlaceholderString = "Search . . .",
|
||||||
|
};
|
||||||
|
textInputNode.AttachNode(this);
|
||||||
|
|
||||||
|
helpNode = new SimpleImageNode {
|
||||||
|
TexturePath = "ui/uld/CircleButtons.tex",
|
||||||
|
TextureCoordinates = new Vector2(112.0f, 84.0f),
|
||||||
|
TextureSize = new Vector2(28.0f, 28.0f),
|
||||||
|
Tooltip = new SeStringBuilder()
|
||||||
|
.Append("Supports Regex Search")
|
||||||
|
.AppendNewLine()
|
||||||
|
.Append("Start input with '$' to search by description")
|
||||||
|
.ToReadOnlySeString(),
|
||||||
|
};
|
||||||
|
helpNode.AttachNode(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public required Action<ReadOnlySeString>? OnInputReceived {
|
||||||
|
get => textInputNode.OnInputReceived;
|
||||||
|
set => textInputNode.OnInputReceived = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnSizeChanged() {
|
||||||
|
base.OnSizeChanged();
|
||||||
|
|
||||||
|
helpNode.Size = new Vector2(Height, Height);
|
||||||
|
helpNode.Position = new Vector2(Width - helpNode.Width - 5.0f, 0.0f);
|
||||||
|
|
||||||
|
textInputNode.Size = new Vector2(Width - helpNode.Width - 5.0f, Height);
|
||||||
|
textInputNode.Position = new Vector2(0.0f, 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlySeString SearchString {
|
||||||
|
get => textInputNode.SeString;
|
||||||
|
set => textInputNode.SeString = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AetherBags.Addons;
|
using AetherBags.Addons;
|
||||||
using AetherBags.Helpers;
|
using AetherBags.Helpers;
|
||||||
using Dalamud.Game.Addon.Lifecycle;
|
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using Dalamud.Game.Command;
|
using Dalamud.Game.Command;
|
||||||
using KamiToolKit;
|
using KamiToolKit;
|
||||||
@@ -43,7 +42,6 @@ public class Plugin : IDalamudPlugin
|
|||||||
if (Services.ClientState.IsLoggedIn) {
|
if (Services.ClientState.IsLoggedIn) {
|
||||||
Services.Framework.RunOnFrameworkThread(OnLogin);
|
Services.Framework.RunOnFrameworkThread(OnLogin);
|
||||||
}
|
}
|
||||||
Services.AddonLifecycle.LogAddon("Inventory");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
Reference in New Issue
Block a user