diff --git a/AetherBags/Addons/AddonCategoryConfigurationWindow.cs b/AetherBags/Addons/AddonCategoryConfigurationWindow.cs index 484e1e4..6ea3348 100644 --- a/AetherBags/Addons/AddonCategoryConfigurationWindow.cs +++ b/AetherBags/Addons/AddonCategoryConfigurationWindow.cs @@ -14,7 +14,7 @@ namespace AetherBags.Addons; public class AddonCategoryConfigurationWindow : NativeAddon { - private ModifyListNode? _selectionListNode; + private ModifyListNode? _selectionListNode; private VerticalLineNode? _separatorLine; private CategoryConfigurationNode? _configNode; private TextNode? _nothingSelectedTextNode; @@ -28,21 +28,24 @@ public class AddonCategoryConfigurationWindow : NativeAddon { _categoryWrappers = CreateCategoryWrappers(); - _selectionListNode = new ModifyListNode + _selectionListNode = new ModifyListNode { Position = ContentStartPosition, - Size = new Vector2(250.0f, ContentSize.Y), - SelectionOptions = _categoryWrappers, - OnOptionChanged = OnOptionChanged, + Size = ContentSize with { X = 250.0f }, + Options = _categoryWrappers, + SelectionChanged = OnOptionChanged, AddNewEntry = OnAddNewCategory, RemoveEntry = OnRemoveCategory, + SortOptions = [ "Order" ], + ItemComparer = (left, right, mode) => left.Compare(right, mode), + IsSearchMatch = (data, search) => data.GetLabel().Contains(search, global::System.StringComparison.OrdinalIgnoreCase) }; _selectionListNode.AttachNode(this); _separatorLine = new VerticalLineNode { Position = ContentStartPosition + new Vector2(250.0f + 8.0f, 0.0f), - Size = new Vector2(4.0f, ContentSize.Y), + Size = ContentSize with { X = 4.0f }, }; _separatorLine.AttachNode(this); @@ -78,6 +81,24 @@ public class AddonCategoryConfigurationWindow : NativeAddon .ToList(); } + private void OnAddNewCategory() + { + var newCategory = new UserCategoryDefinition + { + Name = $"New Category {System.Config.Categories.UserCategories.Count + 1}", + Order = System.Config.Categories.UserCategories.Count, + }; + + System.Config.Categories.UserCategories.Add(newCategory); + + var newWrapper = new CategoryWrapper(newCategory); + _categoryWrappers.Add(newWrapper); + + RefreshSelectionList(); + _selectionListNode?.RefreshList(); + InventoryOrchestrator.RefreshAll(updateMaps: true); + } + private void OnOptionChanged(CategoryWrapper? newOption) { if (_configNode is null) return; @@ -99,29 +120,11 @@ public class AddonCategoryConfigurationWindow : NativeAddon if (_pendingSelectionListRefresh) { _pendingSelectionListRefresh = false; - _selectionListNode?.UpdateList(); + _selectionListNode?.RefreshList(); } } } - private void OnAddNewCategory(ModifyListNode listNode) - { - var newCategory = new UserCategoryDefinition - { - Name = $"New Category {System.Config.Categories.UserCategories.Count + 1}", - Order = System.Config.Categories.UserCategories.Count, - }; - - System.Config.Categories.UserCategories.Add(newCategory); - - var newWrapper = new CategoryWrapper(newCategory); - _categoryWrappers.Add(newWrapper); - listNode.AddOption(newWrapper); - - RefreshSelectionList(); - InventoryOrchestrator.RefreshAll(updateMaps: true); - } - private void OnRemoveCategory(CategoryWrapper categoryWrapper) { if (categoryWrapper.CategoryDefinition is null) return; @@ -146,6 +149,15 @@ public class AddonCategoryConfigurationWindow : NativeAddon return; } - _selectionListNode?.UpdateList(); + _selectionListNode?.RefreshList(); + } + + protected override unsafe void OnFinalize(AtkUnitBase* addon) + { + _selectionListNode = null; + _configNode = null; + _separatorLine = null; + _nothingSelectedTextNode = null; + base.OnFinalize(addon); } } \ No newline at end of file diff --git a/AetherBags/Addons/AddonConfigurationWindow.cs b/AetherBags/Addons/AddonConfigurationWindow.cs index bf95edd..678ae0b 100644 --- a/AetherBags/Addons/AddonConfigurationWindow.cs +++ b/AetherBags/Addons/AddonConfigurationWindow.cs @@ -10,11 +10,11 @@ namespace AetherBags.Addons; public class AddonConfigurationWindow : NativeAddon { - private TabBarNode _tabBarNode = null!; + private TabBarNode? _tabBarNode; - private GeneralScrollingAreaNode _generalScrollingAreaNode = null!; - private CategoryScrollingAreaNode _categoryScrollingAreaNode = null!; - private CurrencyScrollingAreaNode _currencyScrollingAreaNode = null!; + private GeneralScrollingAreaNode? _generalScrollingAreaNode; + private CategoryScrollingAreaNode? _categoryScrollingAreaNode; + private CurrencyScrollingAreaNode? _currencyScrollingAreaNode; private readonly List _tabContent = new(); @@ -73,4 +73,17 @@ public class AddonConfigurationWindow : NativeAddon for (var i = 0; i < _tabContent.Count; i++) _tabContent[i].IsVisible = i == index; } + + protected override unsafe void OnFinalize(AtkUnitBase* addon) + { + _tabBarNode?.Dispose(); + _tabBarNode = null; + _generalScrollingAreaNode?.Dispose(); + _generalScrollingAreaNode = null; + _categoryScrollingAreaNode?.Dispose(); + _categoryScrollingAreaNode = null; + _currencyScrollingAreaNode?.Dispose(); + _currencyScrollingAreaNode = null; + base.OnFinalize(addon); + } } \ No newline at end of file diff --git a/AetherBags/Addons/AddonCurrencyPicker.cs b/AetherBags/Addons/AddonCurrencyPicker.cs new file mode 100644 index 0000000..0804191 --- /dev/null +++ b/AetherBags/Addons/AddonCurrencyPicker.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq; +using AetherBags.Currency; +using KamiToolKit.Premade.ListItemNodes; +using KamiToolKit.Premade.SearchAddons; +using Lumina.Excel.Sheets; + +namespace AetherBags.Addons; + +public class AddonCurrencyPicker : BaseSearchAddon { + public AddonCurrencyPicker() { + var allItems = Services.DataManager.GetExcelSheet(); + var obsoleteTomes = Services.DataManager.GetExcelSheet() + .Where(t => t.Tomestones.RowId == 0) + .Select(t => t.Item.RowId).ToHashSet(); + + var currentTomestones = CurrencyState.GetCurrentTomestoneIds(); + + SearchOptions = allItems + .Where(i => (i.ItemUICategory.RowId == 100 || (i.RowId >= 1 && i.RowId < 100)) && !i.Name.IsEmpty) + .Where(i => !obsoleteTomes.Contains(i.RowId)) + .Where(i => i.RowId != currentTomestones.Limited && i.RowId != currentTomestones.NonLimited) + .ToList(); + } + + protected override bool IsMatch(Item item, string search) => item.Name.ToString().Contains(search, StringComparison.OrdinalIgnoreCase); + protected override int Comparer(Item l, Item r, string s, bool rev) => string.CompareOrdinal(l.Name.ToString(), r.Name.ToString()); +} \ No newline at end of file diff --git a/AetherBags/Addons/AddonInventoryWindow.cs b/AetherBags/Addons/AddonInventoryWindow.cs index b0b6934..af9eff0 100644 --- a/AetherBags/Addons/AddonInventoryWindow.cs +++ b/AetherBags/Addons/AddonInventoryWindow.cs @@ -79,7 +79,7 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase LayoutContent(); - //addon->SubscribeAtkArrayData(1, (int)NumberArrayType.Inventory); + addon->SubscribeAtkArrayData(1, (int)NumberArrayType.Inventory); System.LootedItemsTracker.OnLootedItemsChanged += OnLootedItemsChanged; @@ -139,6 +139,7 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase private void OnClearAllLootedItems() { System.LootedItemsTracker.Clear(); + System.LootedItemsTracker.FlushPendingChanges(); } public void ManualCurrencyRefresh() @@ -155,7 +156,6 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase }, delayTicks: 3); } - protected override void OnFinalize(AtkUnitBase* addon) { System.LootedItemsTracker.OnLootedItemsChanged -= OnLootedItemsChanged; diff --git a/AetherBags/Addons/AddonItemPicker.cs b/AetherBags/Addons/AddonItemPicker.cs new file mode 100644 index 0000000..341f350 --- /dev/null +++ b/AetherBags/Addons/AddonItemPicker.cs @@ -0,0 +1,7 @@ +using KamiToolKit.Premade.ListItemNodes; +using KamiToolKit.Premade.SearchAddons; + +namespace AetherBags.Addons; + +public class AddonItemPicker : ItemSearchAddonBase { +} \ No newline at end of file diff --git a/AetherBags/Addons/AddonUICategoryPicker.cs b/AetherBags/Addons/AddonUICategoryPicker.cs new file mode 100644 index 0000000..4f3fb91 --- /dev/null +++ b/AetherBags/Addons/AddonUICategoryPicker.cs @@ -0,0 +1,14 @@ +using System.Diagnostics.CodeAnalysis; +using AetherBags.Nodes.Configuration.Category; +using KamiToolKit.Premade.SearchAddons; +using Lumina.Excel.Sheets; + +namespace AetherBags.Addons; + +public class AddonUICategoryPicker : BaseSearchAddon { + protected override int Comparer(ItemUICategory left, ItemUICategory right, string sort, bool rev) + => string.CompareOrdinal(left.Name.ToString(), right.Name.ToString()); + + protected override bool IsMatch(ItemUICategory item, string search) + => item.Name.ToString().Contains(search, global::System.StringComparison.OrdinalIgnoreCase); +} \ No newline at end of file diff --git a/AetherBags/Addons/CategoryListItemNode.cs b/AetherBags/Addons/CategoryListItemNode.cs new file mode 100644 index 0000000..b6d6a9c --- /dev/null +++ b/AetherBags/Addons/CategoryListItemNode.cs @@ -0,0 +1,14 @@ +using KamiToolKit.Premade.GenericListItemNodes; + +namespace AetherBags.Addons; + +public class CategoryListItemNode : GenericListItemNode +{ + protected override uint GetIconId(CategoryWrapper data) => data.GetIconId() ?? 0; + + protected override string GetLabelText(CategoryWrapper data) => data.GetLabel(); + + protected override string GetSubLabelText(CategoryWrapper data) => data.GetSubLabel(); + + protected override uint? GetId(CategoryWrapper data) => data.GetId(); +} \ No newline at end of file diff --git a/AetherBags/Addons/CategoryWrapper.cs b/AetherBags/Addons/CategoryWrapper.cs index 7729881..a3f78ad 100644 --- a/AetherBags/Addons/CategoryWrapper.cs +++ b/AetherBags/Addons/CategoryWrapper.cs @@ -1,17 +1,14 @@ using AetherBags.Configuration; using AetherBags.Inventory.Categories; -using KamiToolKit.Premade; namespace AetherBags.Addons; -public class CategoryWrapper(UserCategoryDefinition categoryDefinition) : IInfoNodeData +// Removed IInfoNodeData implementation +public class CategoryWrapper(UserCategoryDefinition categoryDefinition) { public UserCategoryDefinition? CategoryDefinition { get; } = categoryDefinition; - public string GetLabel() { - - return CategoryDefinition!.Name; - } + public string GetLabel() => CategoryDefinition!.Name; public string GetSubLabel() { if(UserCategoryMatcher.IsCatchAll(CategoryDefinition!)) return " No valid rules!"; @@ -20,16 +17,9 @@ public class CategoryWrapper(UserCategoryDefinition categoryDefinition) : IInfoN public uint? GetId() => null; - public uint? GetIconId() { - return 0; - } + public uint? GetIconId() => 0; - public string? GetTexturePath() - => null; - - public int Compare(IInfoNodeData other, string sortingMode) { - if (other is not CategoryWrapper otherWrapper) return 0; - - return CategoryDefinition!.Order.CompareTo(otherWrapper.CategoryDefinition!.Order); + public int Compare(CategoryWrapper other, string sortingMode) { + return CategoryDefinition!.Order.CompareTo(other.CategoryDefinition!.Order); } } \ No newline at end of file diff --git a/AetherBags/Addons/InventoryAddonBase.cs b/AetherBags/Addons/InventoryAddonBase.cs index eff8a21..df7ed7a 100644 --- a/AetherBags/Addons/InventoryAddonBase.cs +++ b/AetherBags/Addons/InventoryAddonBase.cs @@ -9,6 +9,7 @@ using AetherBags.Inventory.Context; using AetherBags.Inventory.Items; using AetherBags.Inventory.Scanning; using AetherBags.Inventory.State; +using AetherBags.Monitoring; using AetherBags.Nodes.Input; using AetherBags.Nodes.Inventory; using AetherBags.Nodes.Layout; @@ -16,7 +17,7 @@ using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit; using KamiToolKit.Classes; -using KamiToolKit.Classes.ContextMenu; +using KamiToolKit.ContextMenu; using KamiToolKit.Nodes; namespace AetherBags.Addons; @@ -65,25 +66,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow private int _refreshFromLifecycleCount; private long _lastLogTick; - public void ManualRefresh() - { - if (!IsOpen) return; - if (!Services.ClientState.IsLoggedIn) return; - if (_isRefreshing) return; - if (!IsSetupComplete) return; - - try - { - _isRefreshing = true; - InventoryState.RefreshFromGame(); - RefreshCategoriesCore(autosize: true); - } - finally - { - _isRefreshing = false; - } - } - + public void ManualRefresh() => ExecuteRefresh(true); public string GetSearchText() => SearchInputNode?.SearchString.ExtractText() ?? string.Empty; @@ -98,21 +81,16 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow }, delayTicks: 3); } - public void RefreshFromLifecycle() + private void ExecuteRefresh(bool autosize) { - if (!IsSetupComplete) return; - if (!IsOpen) return; - if (_isRefreshing) return; + if (!IsSetupComplete || !IsOpen || _isRefreshing) return; try { _isRefreshing = true; - - _refreshFromLifecycleCount++; - LogRefreshStats(); - InventoryState.RefreshFromGame(); - RefreshCategoriesCore(autosize: true); + System.LootedItemsTracker.FlushPendingChanges(); + RefreshCategoriesCore(autosize); } finally { @@ -120,6 +98,8 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow } } + public void RefreshFromLifecycle() => ExecuteRefresh(autosize: true); + protected virtual void RefreshCategoriesCore(bool autosize) { if (!IsSetupComplete) @@ -215,25 +195,22 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow { var header = addon->WindowHeaderCollisionNode; float headerW = header->Width; - float headerH = header->Height; - // Center the search bar, width is 50% of header - float searchWidth = headerW * 0.5f; - var searchSize = new Vector2(searchWidth, 28f); + float settingsX = headerW - 62f; + float itemY = header->Y + (header->Height - 28f) * 0.5f; + float searchWidth = headerW * 0.45f; float searchX = (headerW - searchWidth) * 0.5f; - float itemY = header->Y + (headerH - 28f) * 0.5f; return new HeaderLayout { SearchPosition = new Vector2(searchX, itemY), - SearchSize = searchSize, + SearchSize = new Vector2(searchWidth, 28f), HeaderWidth = headerW, HeaderY = itemY }; } - protected void InitializeBackgroundDropTarget() { BackgroundDropTarget = new DragDropNode @@ -383,6 +360,11 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow float requiredWidth = maxChildWidth + (ContentStartPosition.X * 2); float finalWidth = Math.Clamp(requiredWidth, MinWindowWidth, MaxWindowWidth); + if (SettingsButtonNode != null) + { + SettingsButtonNode.X = finalWidth - 62f; + } + float contentWidth = finalWidth - (ContentStartPosition.X * 2); float footerSpace = HasFooter || HasSlotCounter ? FooterHeight + FooterTopSpacing : 0; @@ -442,21 +424,15 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow } } - /* + protected override void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { - _requestedUpdateCount++; - LogRefreshStats(); - base.OnRequestedUpdate(addon, numberArrayData, stringArrayData); - if (DragDropState.IsDragging) - return; - - InventoryState.RefreshFromGame(); - RefreshCategoriesCore(autosize: true); + if (DragDropState.IsDragging) return; + ExecuteRefresh(autosize: true); } - */ + protected override void OnSetup(AtkUnitBase* addon) { diff --git a/AetherBags/Addons/InventoryAddonContextMenu.cs b/AetherBags/Addons/InventoryAddonContextMenu.cs index 3824ded..366be17 100644 --- a/AetherBags/Addons/InventoryAddonContextMenu.cs +++ b/AetherBags/Addons/InventoryAddonContextMenu.cs @@ -2,7 +2,7 @@ using AetherBags.Configuration; using AetherBags.Inventory; using AetherBags.Inventory.Context; using FFXIVClientStructs.FFXIV.Client.UI.Agent; -using KamiToolKit.Classes.ContextMenu; +using KamiToolKit.ContextMenu; namespace AetherBags.Addons; diff --git a/AetherBags/Configuration/CategorySettings.cs b/AetherBags/Configuration/CategorySettings.cs index 9d288b8..0dedceb 100644 --- a/AetherBags/Configuration/CategorySettings.cs +++ b/AetherBags/Configuration/CategorySettings.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Numerics; using System.Text.Json.Serialization; using KamiToolKit.Classes; @@ -84,6 +85,9 @@ public enum ToggleFilterState public enum PluginFilterMode { + [Description("Create New Categories")] Categorize = 0, + + [Description("Apply Highlight Only")] Highlight = 1, } \ No newline at end of file diff --git a/AetherBags/Configuration/CurrencySettings.cs b/AetherBags/Configuration/CurrencySettings.cs index 505ef00..a0e8236 100644 --- a/AetherBags/Configuration/CurrencySettings.cs +++ b/AetherBags/Configuration/CurrencySettings.cs @@ -1,11 +1,20 @@ +using System.Collections.Generic; using System.Numerics; +using System.Text.Json.Serialization; using KamiToolKit.Classes; namespace AetherBags.Configuration; public class CurrencySettings { + [JsonIgnore] + public const uint LimitedTomestoneId = 0xFFFF_FFFE; + + [JsonIgnore] + public const uint NonLimitedTomestoneId = 0xFFFF_FFFD; + public bool Enabled { get; set; } = true; + public List DisplayedCurrencies { get; set; } = new() { 1, LimitedTomestoneId, NonLimitedTomestoneId }; public bool ColorWhenCapped { get; set; } = true; public bool ColorWhenLimited { get; set; } = true; public Vector4 DefaultColor { get; set; } = ColorHelper.GetColor(8); diff --git a/AetherBags/Configuration/GeneralSettings.cs b/AetherBags/Configuration/GeneralSettings.cs index 79583f5..a68e97c 100644 --- a/AetherBags/Configuration/GeneralSettings.cs +++ b/AetherBags/Configuration/GeneralSettings.cs @@ -1,3 +1,5 @@ +using System.ComponentModel; + namespace AetherBags.Configuration; public class GeneralSettings @@ -21,12 +23,18 @@ public class GeneralSettings public enum InventoryStackMode : byte { + [Description("Split Stacks (Game Default)")] NaturalStacks = 0, + + [Description("Merge Stacks (By Item ID)")] AggregateByItemId = 1, } public enum SearchMode : byte { + [Description("Filter (Hide non-matches)")] Filter = 0, + + [Description("Highlight (Dim non-matches)")] Highlight = 1, } \ No newline at end of file diff --git a/AetherBags/Currency/CurrencyState.cs b/AetherBags/Currency/CurrencyState.cs index a357bab..40242b0 100644 --- a/AetherBags/Currency/CurrencyState.cs +++ b/AetherBags/Currency/CurrencyState.cs @@ -71,6 +71,31 @@ public static unsafe class CurrencyState return currencyInfoList; } + public static (uint Limited, uint NonLimited) GetCurrentTomestoneIds() + { + var tomestonesItemSheet = Services.DataManager.GetExcelSheet(); + uint limitedId = 0; + uint nonLimitedId = 0; + + foreach (var row in tomestonesItemSheet) + { + var tomeSheetRef = row.Tomestones.ValueNullable; + + if (tomeSheetRef == null || tomeSheetRef.Value.RowId == 0) continue; + + var itemId = row.Item.RowId; + if (itemId == 0 || itemId == 28) continue; + + if (tomeSheetRef.Value.WeeklyLimit > 0) + limitedId = itemId; + else + nonLimitedId = itemId; + } + + return (limitedId, nonLimitedId); + } + + /* private static uint? GetLimitedTomestoneItemIdCached() { if (_cachedLimitedTomestoneItemId.HasValue) @@ -96,6 +121,13 @@ public static unsafe class CurrencyState _cachedNonLimitedTomestoneItemId = itemId; return itemId; } + */ + + private static uint? GetLimitedTomestoneItemIdCached() + => _cachedLimitedTomestoneItemId ??= GetCurrentTomestoneIds().Limited; + + private static uint? GetNonLimitedTomestoneItemIdCached() + => _cachedNonLimitedTomestoneItemId ??= GetCurrentTomestoneIds().NonLimited; private static CurrencyItem ResolveCurrencyItemIdCached(uint currencyId) { diff --git a/AetherBags/Extensions/EnumExtensions.cs b/AetherBags/Extensions/EnumExtensions.cs new file mode 100644 index 0000000..258ba19 --- /dev/null +++ b/AetherBags/Extensions/EnumExtensions.cs @@ -0,0 +1,52 @@ +using System; +using System.ComponentModel; +using System.Numerics; +using System.Runtime.CompilerServices; +using Dalamud.Utility; + +namespace AetherBags.Extensions; + +internal static class EnumExtensions { + extension(Enum enumValue) { + public string Description => enumValue.GetDescription(); + + private string GetDescription() { + var attribute = enumValue.GetAttribute(); + return attribute?.Description ?? enumValue.ToString(); + } + } + + extension(ref T flagValue) where T : unmanaged, Enum { + public void SetFlags(params T[] flags) { + foreach (var flag in flags) { + flagValue.SetFlag(flag, true); + } + } + + public void ClearFlags(params T[] flags) { + foreach (var flag in flags) { + flagValue.SetFlag(flag, false); + } + } + + private unsafe void SetFlag(T flag, bool enable) { + switch (sizeof(T)) { + case 1: flagValue.SetFlag(flag, enable); break; + case 2: flagValue.SetFlag(flag, enable); break; + case 4: flagValue.SetFlag(flag, enable); break; + case 8: flagValue.SetFlag(flag, enable); break; + default: throw new NotSupportedException("Unsupported enum size"); + } + } + + private void SetFlag(T flag, bool enable) where TUnderlying : unmanaged, IBinaryInteger { + ref var value = ref Unsafe.As(ref flagValue); + var mask = Unsafe.As(ref flag); + + if (enable) + value |= mask; + else + value &= ~mask; + } + } +} diff --git a/AetherBags/Extensions/LoggerExtensions.cs b/AetherBags/Extensions/LoggerExtensions.cs index dce0eae..3758e25 100644 --- a/AetherBags/Extensions/LoggerExtensions.cs +++ b/AetherBags/Extensions/LoggerExtensions.cs @@ -1,22 +1,27 @@ +using System.Diagnostics; +using Dalamud.Plugin.Services; + namespace AetherBags.Extensions; public static class LoggerExtensions { - extension(object logger) + extension(IPluginLog logger) { + [Conditional("DEBUG")] public void DebugOnly(string message) { if (System.Config?.General?.DebugEnabled == true) { - Services.Logger.Debug(message); + logger.Debug(message); } } + [Conditional("DEBUG")] public void DebugOnly(string message, params object[] args) { if (System.Config?.General?.DebugEnabled == true) { - Services.Logger.Debug(message, args); + logger.Debug(message, args); } } } diff --git a/AetherBags/Inventory/State/InventoryStateBase.cs b/AetherBags/Inventory/State/InventoryStateBase.cs index 9f0ed23..215ce69 100644 --- a/AetherBags/Inventory/State/InventoryStateBase.cs +++ b/AetherBags/Inventory/State/InventoryStateBase.cs @@ -18,6 +18,7 @@ public abstract class InventoryStateBase protected readonly List AllCategories = new(capacity: 256); protected readonly List FilteredCategories = new(capacity: 256); protected readonly List UserCategoriesSortedScratch = new(capacity: 64); + protected readonly List EnabledUserCategoriesScratch = new(capacity: 64); protected readonly List RemoveKeysScratch = new(capacity: 256); protected readonly HashSet ClaimedKeys = new(capacity: 512); @@ -68,18 +69,22 @@ public abstract class InventoryStateBase bool allaganCategoriesEnabled = config.Categories.AllaganToolsCategoriesEnabled && categoriesEnabled; bool bisCategoriesEnabled = config.Categories.BisBuddyEnabled && categoriesEnabled; // TODO: Cache this when config changes - UserCategoriesSortedScratch.Clear(); - foreach (var cat in config.Categories. UserCategories) + EnabledUserCategoriesScratch.Clear(); + foreach (var cat in config.Categories.UserCategories) { if (cat.Enabled) - UserCategoriesSortedScratch.Add(cat); + EnabledUserCategoriesScratch.Add(cat); } - var userCategories = UserCategoriesSortedScratch; - if (userCategoriesEnabled && userCategories.Count > 0) + if (userCategoriesEnabled && EnabledUserCategoriesScratch.Count > 0) { CategoryBucketManager.BucketByUserCategories( - ItemInfoByKey, userCategories, BucketsByKey, ClaimedKeys, UserCategoriesSortedScratch); + ItemInfoByKey, + EnabledUserCategoriesScratch, + BucketsByKey, + ClaimedKeys, + UserCategoriesSortedScratch + ); } if (allaganCategoriesEnabled) diff --git a/AetherBags/AddonLifecycles/InventoryLifecycles.cs b/AetherBags/Monitoring/InventoryMonitor.cs similarity index 81% rename from AetherBags/AddonLifecycles/InventoryLifecycles.cs rename to AetherBags/Monitoring/InventoryMonitor.cs index 4e35a9b..fb8523b 100644 --- a/AetherBags/AddonLifecycles/InventoryLifecycles.cs +++ b/AetherBags/Monitoring/InventoryMonitor.cs @@ -1,18 +1,22 @@ using System; +using System.Collections.Generic; using System.Linq; using AetherBags.Configuration; using AetherBags.Inventory.Context; +using AetherBags.Inventory.Scanning; using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.Inventory.InventoryEventArgTypes; using Dalamud.Game.NativeWrapper; using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Text.ReadOnly; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; -namespace AetherBags.AddonLifecycles; +namespace AetherBags.Monitoring; public static unsafe class DragDropState { @@ -22,10 +26,10 @@ public static unsafe class DragDropState public static bool IsDragging => AtkStage.Instance()->DragDropManager.IsDragging; } -public class InventoryLifecycles : IDisposable +public class InventoryMonitor : IDisposable { - public InventoryLifecycles() + public InventoryMonitor() { var bags = new[] { "Inventory", "InventoryLarge", "InventoryExpansion" }; var saddle = new[] { "InventoryBuddy" }; @@ -45,8 +49,8 @@ public class InventoryLifecycles : IDisposable Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "InventoryBuddy", OnSaddleBagUpdate); Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, retainer, OnRetainerInventoryUpdate); - // PreShow - Services.AddonLifecycle.RegisterListener(AddonEvent.PreOpen, "InventoryBuddy", OnSaddleBagOpen); + // Dalamud raw event for raw inventory changes (scans once per frame) + Services.GameInventory.InventoryChangedRaw += OnInventoryChangedRaw; Services.Logger.Verbose("InventoryLifecycles initialized"); } @@ -122,6 +126,32 @@ public class InventoryLifecycles : IDisposable values[7] = can use Saddlebags (Agent InventoryBuddy IsActivatable) */ + private void OnInventoryChangedRaw(IReadOnlyCollection events) + { + bool needsRefresh = false; + foreach (var inventoryEventArgs in events) + { + if (InventoryScanner.StandardInventories.Contains((InventoryType)inventoryEventArgs.Item.ContainerType)) + { + needsRefresh = true; + break; + } + } + + if (needsRefresh) + { + Services.Framework.RunOnTick(() => + { + if (IsInUnsafeState() || DragDropState.IsDragging) return; + + System.LootedItemsTracker.FlushPendingChanges(); + System.AddonInventoryWindow?.RefreshFromLifecycle(); + System.AddonSaddleBagWindow?.RefreshFromLifecycle(); + System.AddonRetainerWindow?.RefreshFromLifecycle(); + }); + } + } + private unsafe void InventoryPreRefreshHandler(AddonEvent type, AddonArgs args) { if (args is not AddonRefreshArgs refreshArgs) @@ -165,25 +195,6 @@ public class InventoryLifecycles : IDisposable } } - // TODO: Inventory/Retainers are not perma open, need some way to close it too. - private void InventoryBuddyPreRefreshHandler(AddonEvent type, AddonArgs args) - { - if (args is not AddonRefreshArgs refreshArgs) - return; - - if (IsInUnsafeState()) - return; - - GeneralSettings config = System.Config.General; - - if (config.HideGameSaddleBags) refreshArgs.AtkValueCount = 0; - if (config.OpenSaddleBagsWithGameInventory) - { - System.AddonSaddleBagWindow.Toggle(); - } - } - - private void OnInventoryUpdate(AddonEvent type, AddonArgs args) { if (IsInUnsafeState()) @@ -204,6 +215,7 @@ public class InventoryLifecycles : IDisposable if (DragDropState.IsDragging) return; + System.LootedItemsTracker.FlushPendingChanges(); System.AddonSaddleBagWindow?.RefreshFromLifecycle(); } @@ -215,17 +227,13 @@ public class InventoryLifecycles : IDisposable if (DragDropState.IsDragging) return; + System.LootedItemsTracker.FlushPendingChanges(); System.AddonRetainerWindow?.RefreshFromLifecycle(); } - private void OnSaddleBagOpen(AddonEvent type, AddonArgs args) - { - if (args is not AddonShowArgs showArgs) - return; - } - public void Dispose() { - Services.AddonLifecycle.UnregisterListener(OnPostSetup, OnPreFinalize, OnInventoryUpdate, OnSaddleBagUpdate, OnRetainerInventoryUpdate, OnSaddleBagOpen); + Services.GameInventory.InventoryChangedRaw -= OnInventoryChangedRaw; + Services.AddonLifecycle.UnregisterListener(OnPostSetup, OnPreFinalize, OnInventoryUpdate, OnSaddleBagUpdate, OnRetainerInventoryUpdate); } } \ No newline at end of file diff --git a/AetherBags/Inventory/LootedItemsTracker.cs b/AetherBags/Monitoring/LootedItemsTracker.cs similarity index 66% rename from AetherBags/Inventory/LootedItemsTracker.cs rename to AetherBags/Monitoring/LootedItemsTracker.cs index 5463bc5..a7603cd 100644 --- a/AetherBags/Inventory/LootedItemsTracker.cs +++ b/AetherBags/Monitoring/LootedItemsTracker.cs @@ -9,7 +9,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using Lumina.Excel.Sheets; -namespace AetherBags.Inventory; +namespace AetherBags.Monitoring; public sealed unsafe class LootedItemsTracker : IDisposable { @@ -32,6 +32,8 @@ public sealed unsafe class LootedItemsTracker : IDisposable public bool HasPendingChanges => _pendingChanges.Count > 0 || _hasPendingRemoval; + private int GetNextIndex() => _lootedItems.Count > 0 ? _lootedItems.Max(x => x.Index) + 1 : 0; + public void Enable() { if (_isEnabled) return; @@ -81,6 +83,8 @@ public sealed unsafe class LootedItemsTracker : IDisposable { if (_pendingChanges.Count == 0 && !_hasPendingRemoval) return; + ProcessPendingChanges(); + _hasPendingRemoval = false; OnLootedItemsChanged?.Invoke(_lootedItems); } @@ -90,12 +94,40 @@ public sealed unsafe class LootedItemsTracker : IDisposable Disable(); } + private void ProcessPendingChanges() + { + if (_pendingChanges.Count == 0) return; + + foreach (var ((itemId, isHq), (item, delta)) in _pendingChanges) + { + int existingIndex = _lootedItems.FindIndex(x => + x.Item.ItemId == itemId && + x.Item.Flags.HasFlag(InventoryItem.ItemFlags.HighQuality) == isHq); + + if (existingIndex >= 0) + { + var current = _lootedItems[existingIndex]; + int newQty = current.Quantity + delta; + + if (newQty <= 0) + _lootedItems.RemoveAt(existingIndex); + else + _lootedItems[existingIndex] = current with { Quantity = newQty }; + } + else if (delta > 0) + { + _lootedItems.Add(new LootedItemInfo(GetNextIndex(), item, delta)); + } + } + + _pendingChanges.Clear(); + } + private void OnInventoryChangedRaw(IReadOnlyCollection events) { - if (!_isEnabled) return; - if (!Services.ClientState.IsLoggedIn) return; + if (!_isEnabled || !Services.ClientState.IsLoggedIn) return; - bool anyAdded = false; + bool anyChanged = false; foreach (var eventData in events) { @@ -105,38 +137,42 @@ public sealed unsafe class LootedItemsTracker : IDisposable if (eventData.Item.ContainerType == GameInventoryType.DamagedGear) continue; - if (eventData is not (InventoryItemAddedArgs or InventoryItemChangedArgs)) - continue; - - if (eventData is InventoryItemChangedArgs changedArgs && - changedArgs.OldItemState.Quantity >= changedArgs.Item.Quantity) + int changeAmount = eventData switch { - continue; - } + InventoryItemAddedArgs added => added.Item.Quantity, + InventoryItemRemovedArgs removed => -removed.Item.Quantity, + InventoryItemChangedArgs changed => changed.Item.Quantity - changed.OldItemState.Quantity, + _ => 0 + }; + + if (changeAmount == 0) continue; if (ShouldFilterItem(eventData.Item.ItemId)) continue; - var inventoryItem = *(InventoryItem*)eventData.Item.Address; - var changeAmount = eventData is InventoryItemChangedArgs changed - ? changed.Item.Quantity - changed.OldItemState.Quantity - : eventData.Item.Quantity; - - var key = (inventoryItem.ItemId, IsHq: inventoryItem.Flags.HasFlag(InventoryItem.ItemFlags.HighQuality)); + uint itemId = eventData.Item.ItemId; + bool isHq = eventData.Item.IsHq; + var key = (itemId, isHq); if (_pendingChanges.TryGetValue(key, out var existing)) { - _pendingChanges[key] = (inventoryItem, existing.Quantity + changeAmount); + _pendingChanges[key] = (existing.Item, existing.Quantity + changeAmount); } else { - _pendingChanges[key] = (inventoryItem, changeAmount); + InventoryItem itemStruct = default; + if (changeAmount > 0) + { + itemStruct = *(InventoryItem*)eventData.Item.Address; + } + + _pendingChanges[key] = (itemStruct, changeAmount); } - anyAdded = true; + anyChanged = true; } - if (anyAdded && _batchStartTick == 0) + if (anyChanged && _batchStartTick == 0) { _batchStartTick = Environment.TickCount64; } @@ -152,23 +188,7 @@ public sealed unsafe class LootedItemsTracker : IDisposable _batchStartTick = 0; - if (_pendingChanges.Count == 0) - return; - - foreach (var ((itemId, isHq), (item, quantity)) in _pendingChanges) - { - if (quantity <= 0) - continue; - - _lootedItems.Add(new LootedItemInfo( - _lootedItems.Count, - item, - quantity)); - } - - _pendingChanges.Clear(); - - OnLootedItemsChanged?.Invoke(_lootedItems); + FlushPendingChanges(); } private static bool ShouldFilterItem(uint itemId) diff --git a/AetherBags/Nodes/Color/ColorInputRow.cs b/AetherBags/Nodes/Color/ColorInputRow.cs index 1eb082f..582d2bd 100644 --- a/AetherBags/Nodes/Color/ColorInputRow.cs +++ b/AetherBags/Nodes/Color/ColorInputRow.cs @@ -3,6 +3,7 @@ using System.Numerics; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Nodes; using KamiToolKit.Premade.Addons; +using KamiToolKit.Premade.Color; namespace AetherBags.Nodes.Color; @@ -16,41 +17,49 @@ public class ColorInputRow : HorizontalListNode { InitializeColorPicker(); - var initialColor = CurrentColor; - - _colorPreview = new ColorPreviewButtonNode - { - Size = new Vector2(28), - Color = CurrentColor, - OnClick = () => - { - _colorPickerAddon?.InitialColor = CurrentColor; - _colorPickerAddon?.DefaultColor = DefaultColor; - _colorPickerAddon?.Toggle(); - _colorPickerAddon?.OnColorConfirmed = color => - { - CurrentColor = color; - _colorPreview?.Color = color; - initialColor = color; - OnColorConfirmed?.Invoke(color); - }; - _colorPickerAddon?.OnColorPreviewed = color => - { - _colorPreview?.Color = color; - OnColorChange?.Invoke(color); - }; - _colorPickerAddon?.OnColorCancelled = () => OnColorCanceled?.Invoke(initialColor); - } - }; - _colorPreview.AttachNode(this); - + _colorPreview = new ColorPreviewButtonNode { Size = new Vector2(28) }; _labelTextNode = new LabelTextNode { TextFlags = TextFlags.AutoAdjustNodeSize, Position = new Vector2(28, 0), - Height = 24, - String = Label ?? string.Empty, + Height = 28, }; + + var node = _colorPreview; + + node.OnClick = () => + { + var snapshot = CurrentColor; + + if (_colorPickerAddon is not null) + { + _colorPickerAddon.InitialColor = snapshot; + _colorPickerAddon.DefaultColor = DefaultColor; + _colorPickerAddon.Toggle(); + + _colorPickerAddon.OnColorConfirmed = color => + { + CurrentColor = color; + node.Color = color; + OnColorConfirmed?.Invoke(color); + }; + + _colorPickerAddon.OnColorPreviewed = color => + { + node.Color = color; + OnColorPreviewed?.Invoke(color); + }; + + _colorPickerAddon.OnColorCancelled = () => + { + CurrentColor = snapshot; + node.Color = snapshot; + OnColorCanceled?.Invoke(snapshot); + }; + } + }; + + _colorPreview.AttachNode(this); _labelTextNode.AttachNode(this); } diff --git a/AetherBags/Nodes/Color/ColorPreviewNode.cs b/AetherBags/Nodes/Color/ColorPreviewNode.cs index 871c896..d929efa 100644 --- a/AetherBags/Nodes/Color/ColorPreviewNode.cs +++ b/AetherBags/Nodes/Color/ColorPreviewNode.cs @@ -2,7 +2,7 @@ using System.Drawing; using System.IO; using System.Numerics; using Dalamud.Interface; -using KamiToolKit.Classes; +using KamiToolKit.Enums; using KamiToolKit.Nodes; namespace AetherBags.Nodes.Color; diff --git a/AetherBags/Nodes/Configuration/Category/BasicSettingsSection.cs b/AetherBags/Nodes/Configuration/Category/BasicSettingsSection.cs new file mode 100644 index 0000000..91bea68 --- /dev/null +++ b/AetherBags/Nodes/Configuration/Category/BasicSettingsSection.cs @@ -0,0 +1,141 @@ +using System; +using System.Numerics; +using AetherBags.Configuration; +using AetherBags.Nodes.Color; +using Dalamud.Utility; +using KamiToolKit.Nodes; + +namespace AetherBags.Nodes.Configuration.Category; + +public sealed class BasicSettingsSection(Func getCategoryDefinition) : ConfigurationSection(getCategoryDefinition) +{ + public Action? OnPropertyChanged { get; init; } + + private CheckboxNode? _enabledCheckbox; + private CheckboxNode? _pinnedCheckbox; + private TextInputNode? _nameInput; + private TextInputNode? _descriptionInput; + private ColorInputRow? _colorInput; + private NumericInputNode? _priorityInput; + private NumericInputNode? _orderInput; + + private bool _initialized; + + private void EnsureInitialized() + { + if (_initialized) return; + _initialized = true; + + _enabledCheckbox = new CheckboxNode + { + Size = new Vector2(Width, 20), + String = "Enabled", + OnClick = isChecked => + { + CategoryDefinition.Enabled = isChecked; + OnPropertyChanged?.Invoke(); + }, + }; + AddNode(_enabledCheckbox); + + _pinnedCheckbox = new CheckboxNode + { + Size = new Vector2(Width, 20), + String = "Pinned", + OnClick = isChecked => + { + CategoryDefinition.Pinned = isChecked; + OnPropertyChanged?.Invoke(); + }, + }; + AddNode(_pinnedCheckbox); + + AddNode(CreateLabel("Name: ")); + _nameInput = new TextInputNode + { + Size = new Vector2(250, 28), + PlaceholderString = "Category Name", + OnInputReceived = input => + { + CategoryDefinition.Name = input.ExtractText(); + OnPropertyChanged?.Invoke(); + }, + }; + AddNode(_nameInput); + + AddNode(CreateLabel("Description:")); + _descriptionInput = new TextInputNode + { + Size = new Vector2(250, 28), + PlaceholderString = "Optional description", + OnInputReceived = input => + { + CategoryDefinition.Description = input.ExtractText(); + OnValueChanged?.Invoke(); + }, + }; + AddNode(_descriptionInput); + + _colorInput = new ColorInputRow + { + Label = "Color", + Size = new Vector2(300, 28), + CurrentColor = new UserCategoryDefinition().Color, + DefaultColor = new UserCategoryDefinition().Color, + OnColorConfirmed = color => { CategoryDefinition.Color = color; OnValueChanged?.Invoke(); }, + OnColorCanceled = color => { CategoryDefinition.Color = color; OnValueChanged?.Invoke(); }, + OnColorPreviewed = color => { CategoryDefinition.Color = color; OnValueChanged?.Invoke(); }, + OnColorChange = color => { CategoryDefinition.Color = color; OnValueChanged?.Invoke(); }, + }; + AddNode(_colorInput); + + AddNode(CreateLabel("Priority:")); + _priorityInput = new NumericInputNode + { + Size = new Vector2(120, 28), + Min = 0, + Max = 1000, + Step = 1, + OnValueUpdate = value => + { + CategoryDefinition.Priority = value; + OnValueChanged?.Invoke(); + }, + }; + AddNode(_priorityInput); + + AddNode(CreateLabel("Order: ")); + _orderInput = new NumericInputNode + { + Size = new Vector2(120, 28), + Min = 0, + Max = 9999, + Step = 1, + OnValueUpdate = val => + { + CategoryDefinition.Order = val; + OnPropertyChanged?.Invoke(); + }, + }; + AddNode(_orderInput); + + RecalculateLayout(); + } + + public override void Refresh() + { + EnsureInitialized(); + + _enabledCheckbox!.IsChecked = CategoryDefinition.Enabled; + _pinnedCheckbox!.IsChecked = CategoryDefinition.Pinned; + _nameInput!.String = CategoryDefinition.Name; + _nameInput.PlaceholderString = CategoryDefinition.Name.IsNullOrWhitespace() ? "Category Name" : ""; + _descriptionInput!.String = CategoryDefinition.Description; + _descriptionInput.PlaceholderString = CategoryDefinition.Description.IsNullOrWhitespace() ? "Optional description" : ""; + _colorInput!.CurrentColor = CategoryDefinition.Color; + _priorityInput!.Value = CategoryDefinition.Priority; + _orderInput!.Value = CategoryDefinition.Order; + + RecalculateLayout(); + } +} \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs b/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs index c90a04f..9add9c7 100644 --- a/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs +++ b/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs @@ -3,8 +3,7 @@ using System.Collections.Generic; using System.Numerics; using AetherBags.Configuration; using AetherBags.Inventory; -using AetherBags.Nodes.Color; -using Dalamud.Utility; +using AetherBags.Nodes.Layout; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Nodes; using Lumina.Excel; @@ -23,71 +22,39 @@ public sealed class CategoryDefinitionConfigurationNode : SimpleComponentNode private UserCategoryDefinition _categoryDefinition = new(); - private readonly ScrollingAreaNode _scrollingArea; - private readonly BasicSettingsSection _basicSettings; - private readonly RangeFiltersSection _rangeFilters; - private readonly StateFiltersSection _stateFilters; - private readonly ListFiltersSection _listFilters; + private readonly ScrollingAreaNode _scrollingArea; + private readonly List _sections = new(); public CategoryDefinitionConfigurationNode() { - _scrollingArea = new ScrollingAreaNode - { - ContentHeight = 100.0f, + _scrollingArea = new ScrollingAreaNode { AutoHideScrollBar = true, + ContentHeight = 100f }; _scrollingArea.AttachNode(this); - _scrollingArea.ContentNode.OnLayoutUpdate = newHeight => - { - _scrollingArea.ContentHeight = newHeight; - }; + var list = _scrollingArea.ContentAreaNode; + list.FitContents = true; + list.ItemSpacing = 4.0f; - _scrollingArea.ContentNode.CategoryVerticalSpacing = 4.0f; + _sections.Add(new BasicSettingsSection(() => _categoryDefinition) { + String = "Basic Settings", IsCollapsed = false, + OnPropertyChanged = () => { NotifyChanged(); OnCategoryPropertyChanged?.Invoke(); } + }); - var treeListNode = _scrollingArea.ContentAreaNode; - - _basicSettings = new BasicSettingsSection(() => _categoryDefinition) - { - String = "Basic Settings", - IsCollapsed = false, - OnPropertyChanged = () => - { - NotifyChanged(); - NotifyCategoryPropertyChanged(); - }, - OnValueChanged = NotifyChanged, - }; - _basicSettings.OnToggle = _ => HandleLayoutChange(); - treeListNode.AddCategoryNode(_basicSettings); - - _rangeFilters = new RangeFiltersSection(() => _categoryDefinition) - { - String = "Range Filters", - IsCollapsed = true, - OnValueChanged = NotifyChanged, - }; - _rangeFilters.OnToggle = _ => HandleLayoutChange(); - treeListNode.AddCategoryNode(_rangeFilters); - - _stateFilters = new StateFiltersSection(() => _categoryDefinition) - { - String = "State Filters", - IsCollapsed = true, - OnValueChanged = NotifyChanged, - }; - _stateFilters.OnToggle = _ => HandleLayoutChange(); - treeListNode.AddCategoryNode(_stateFilters); - - _listFilters = new ListFiltersSection(() => _categoryDefinition) - { + _sections.Add(new RangeFiltersSection(() => _categoryDefinition) { String = "Range Filters" }); + _sections.Add(new StateFiltersSection(() => _categoryDefinition) { String = "State Filters" }); + _sections.Add(new ListFiltersSection(() => _categoryDefinition) { String = "List Filters", - IsCollapsed = true, - OnValueChanged = NotifyChanged, - OnListChanged = HandleListChanged, - }; - _listFilters.OnToggle = _ => HandleLayoutChange(); - treeListNode.AddCategoryNode(_listFilters); + OnListChanged = HandleLayoutChange + }); + + foreach (var section in _sections) + { + section.OnToggle = HandleLayoutChange; + section.OnValueChanged = NotifyChanged; + list.AddNode(section); + } } protected override void OnSizeChanged() @@ -96,417 +63,56 @@ public sealed class CategoryDefinitionConfigurationNode : SimpleComponentNode _scrollingArea.Size = Size; - foreach (var categoryNode in _scrollingArea.ContentNode.CategoryNodes) + foreach (var section in _sections) { - categoryNode.Width = Width - 16.0f; + section.Width = Width - 16.0f; } - - _scrollingArea.ContentNode.RefreshLayout(); + HandleLayoutChange(); } public void SetCategory(UserCategoryDefinition newCategory) { _categoryDefinition = newCategory; - RefreshAllValues(); - } - - private void RefreshAllValues() - { - _basicSettings.Refresh(); - _rangeFilters.Refresh(); - _stateFilters.Refresh(); - _listFilters.Refresh(); - - HandleLayoutChange(); - } - - private void HandleListChanged() - { - NotifyChanged(); + foreach (var section in _sections) section.Refresh(); HandleLayoutChange(); } private void HandleLayoutChange() { - _scrollingArea.ContentNode.RefreshLayout(); + _scrollingArea.ContentAreaNode.RecalculateLayout(); + _scrollingArea.ContentHeight = _scrollingArea.ContentAreaNode.Height; OnLayoutChanged?.Invoke(); } private static void NotifyChanged() => InventoryOrchestrator.RefreshAll(updateMaps: true); - private void NotifyCategoryPropertyChanged() => OnCategoryPropertyChanged?.Invoke(); - public static string ResolveItemName(uint itemId) => ItemSheet?.GetRow(itemId).Name.ToString() ?? "Unknown"; public static string ResolveUiCategoryName(uint categoryId) => UICategorySheet?.GetRow(categoryId).Name.ToString() ?? "Unknown"; } -public abstract class ConfigurationSection : TreeListCategoryNode +public abstract class ConfigurationSection : CollapsibleSectionNode { private readonly Func _getCategoryDefinition; - public Action? OnValueChanged { get; init; } + public Action? OnValueChanged { get; set; } protected UserCategoryDefinition CategoryDefinition => _getCategoryDefinition(); protected ConfigurationSection(Func getCategoryDefinition) { _getCategoryDefinition = getCategoryDefinition; - VerticalPadding = 4.0f; + HeaderHeight = 30.0f; + + AddTab(); } + public abstract void Refresh(); + protected static LabelTextNode CreateLabel(string text) => new() { TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(80, 20), String = text, }; -} - -public sealed class BasicSettingsSection : ConfigurationSection -{ - public Action? OnPropertyChanged { get; init; } - - private CheckboxNode? _enabledCheckbox; - private CheckboxNode? _pinnedCheckbox; - private TextInputNode? _nameInput; - private TextInputNode? _descriptionInput; - private ColorInputRow? _colorInput; - private NumericInputNode? _priorityInput; - private NumericInputNode? _orderInput; - - private bool _initialized; - - public BasicSettingsSection(Func getCategoryDefinition) - : base(getCategoryDefinition) - { - } - - private void EnsureInitialized() - { - if (_initialized) return; - _initialized = true; - - _enabledCheckbox = new CheckboxNode - { - Size = new Vector2(Width, 20), - String = "Enabled", - OnClick = isChecked => - { - CategoryDefinition.Enabled = isChecked; - OnPropertyChanged?.Invoke(); - }, - }; - AddNode(_enabledCheckbox); - - _pinnedCheckbox = new CheckboxNode - { - Size = new Vector2(Width, 20), - String = "Pinned", - OnClick = isChecked => - { - CategoryDefinition.Pinned = isChecked; - OnPropertyChanged?.Invoke(); - }, - }; - AddNode(_pinnedCheckbox); - - AddNode(CreateLabel("Name: ")); - _nameInput = new TextInputNode - { - Size = new Vector2(250, 28), - PlaceholderString = "Category Name", - OnInputReceived = input => - { - CategoryDefinition.Name = input.ExtractText(); - OnPropertyChanged?.Invoke(); - }, - }; - AddNode(_nameInput); - - AddNode(CreateLabel("Description:")); - _descriptionInput = new TextInputNode - { - Size = new Vector2(250, 28), - PlaceholderString = "Optional description", - OnInputReceived = input => - { - CategoryDefinition.Description = input.ExtractText(); - OnValueChanged?.Invoke(); - }, - }; - AddNode(_descriptionInput); - - _colorInput = new ColorInputRow - { - Label = "Color", - Size = new Vector2(300, 28), - CurrentColor = new UserCategoryDefinition().Color, - DefaultColor = new UserCategoryDefinition().Color, - OnColorConfirmed = c => { CategoryDefinition.Color = c; OnValueChanged?.Invoke(); }, - OnColorCanceled = c => { CategoryDefinition.Color = c; OnValueChanged?.Invoke(); }, - OnColorPreviewed = c => { CategoryDefinition.Color = c; OnValueChanged?.Invoke(); }, - }; - AddNode(_colorInput); - - AddNode(CreateLabel("Priority:")); - _priorityInput = new NumericInputNode - { - Size = new Vector2(120, 28), - Min = 0, - Max = 1000, - Step = 1, - OnValueUpdate = val => - { - CategoryDefinition.Priority = val; - OnValueChanged?.Invoke(); - }, - }; - AddNode(_priorityInput); - - AddNode(CreateLabel("Order: ")); - _orderInput = new NumericInputNode - { - Size = new Vector2(120, 28), - Min = 0, - Max = 9999, - Step = 1, - OnValueUpdate = val => - { - CategoryDefinition.Order = val; - OnPropertyChanged?.Invoke(); - }, - }; - AddNode(_orderInput); - - RecalculateLayout(); - } - - public void Refresh() - { - EnsureInitialized(); - - _enabledCheckbox!.IsChecked = CategoryDefinition.Enabled; - _pinnedCheckbox!.IsChecked = CategoryDefinition.Pinned; - _nameInput!.String = CategoryDefinition.Name; - _nameInput.PlaceholderString = CategoryDefinition.Name.IsNullOrWhitespace() ? "Category Name" : ""; - _descriptionInput!.String = CategoryDefinition.Description; - _descriptionInput.PlaceholderString = CategoryDefinition.Description.IsNullOrWhitespace() ? "Optional description" : ""; - _colorInput!.CurrentColor = CategoryDefinition.Color; - _priorityInput!.Value = CategoryDefinition.Priority; - _orderInput!.Value = CategoryDefinition.Order; - - RecalculateLayout(); - ParentTreeListNode?.RefreshLayout(); - } -} - -public sealed class RangeFiltersSection : ConfigurationSection -{ - private RangeFilterRow? _levelFilter; - private RangeFilterRow? _itemLevelFilter; - private RangeFilterRowUint? _vendorPriceFilter; - - private bool _initialized; - - public RangeFiltersSection(Func getCategoryDefinition) - : base(getCategoryDefinition) - { - } - - private void EnsureInitialized() - { - if (_initialized) return; - _initialized = true; - - _levelFilter = new RangeFilterRow - { - Label = "Level", - MinBound = 0, - MaxBound = 200, - OnFilterChanged = (enabled, min, max) => - { - CategoryDefinition.Rules.Level.Enabled = enabled; - CategoryDefinition.Rules.Level.Min = min; - CategoryDefinition.Rules.Level.Max = max; - OnValueChanged?.Invoke(); - }, - }; - AddNode(_levelFilter); - - _itemLevelFilter = new RangeFilterRow - { - Label = "Item Level", - MinBound = 0, - MaxBound = 2000, - OnFilterChanged = (enabled, min, max) => - { - CategoryDefinition.Rules.ItemLevel.Enabled = enabled; - CategoryDefinition.Rules.ItemLevel.Min = min; - CategoryDefinition.Rules.ItemLevel.Max = max; - OnValueChanged?.Invoke(); - }, - }; - AddNode(_itemLevelFilter); - - _vendorPriceFilter = new RangeFilterRowUint - { - Label = "Vendor Price", - MinBound = 0, - MaxBound = 9_999_999, - OnFilterChanged = (enabled, min, max) => - { - CategoryDefinition.Rules.VendorPrice.Enabled = enabled; - CategoryDefinition.Rules.VendorPrice.Min = min; - CategoryDefinition.Rules.VendorPrice.Max = max; - OnValueChanged?.Invoke(); - }, - }; - AddNode(_vendorPriceFilter); - - RecalculateLayout(); - } - - public void Refresh() - { - EnsureInitialized(); - - _levelFilter!.SetFilter(CategoryDefinition.Rules.Level); - _itemLevelFilter!.SetFilter(CategoryDefinition.Rules.ItemLevel); - _vendorPriceFilter!.SetFilter(CategoryDefinition.Rules.VendorPrice); - - RecalculateLayout(); - ParentTreeListNode?.RefreshLayout(); - } -} - -public sealed class StateFiltersSection : ConfigurationSection -{ - private readonly List<(StateFilterRowNode Node, Func GetFilter)> _filters = []; - private bool _initialized; - - public StateFiltersSection(Func getCategoryDefinition) - : base(getCategoryDefinition) - { - } - - private void EnsureInitialized() - { - if (_initialized) return; - _initialized = true; - - AddFilter("Untradable", def => def.Rules.Untradable); - AddFilter("Unique", def => def.Rules.Unique); - AddFilter("Collectable", def => def.Rules.Collectable); - AddFilter("Dyeable", def => def.Rules.Dyeable); - AddFilter("Repairable", def => def.Rules.Repairable); - AddFilter("High Quality", def => def.Rules.HighQuality); - AddFilter("Desynthesizable", def => def.Rules.Desynthesizable); - AddFilter("Glamourable", def => def.Rules.Glamourable); - AddFilter("Spiritbonded", def => def.Rules.FullySpiritbonded); - - RecalculateLayout(); - } - - private void AddFilter(string label, Func getFilter) - { - var node = new StateFilterRowNode(label, new StateFilter(), () => OnValueChanged?.Invoke()); - _filters.Add((node, getFilter)); - AddNode(node); - } - - public void Refresh() - { - EnsureInitialized(); - - foreach (var (node, getFilter) in _filters) - { - node.SetState(getFilter(CategoryDefinition)); - } - - RecalculateLayout(); - ParentTreeListNode?.RefreshLayout(); - } -} - -public sealed class ListFiltersSection : ConfigurationSection -{ - public Action? OnListChanged { get; init; } - - private UintListEditorNode? _itemIdsEditor; - private StringListEditorNode? _namePatternsEditor; - private UintListEditorNode? _uiCategoriesEditor; - private RarityEditorNode? _raritiesEditor; - - private bool _initialized; - - public ListFiltersSection(Func getCategoryDefinition) - : base(getCategoryDefinition) - { - } - - private void EnsureInitialized() - { - if (_initialized) return; - _initialized = true; - - _itemIdsEditor = new UintListEditorNode - { - Label = "Allowed Item IDs:", - LabelResolver = CategoryDefinitionConfigurationNode.ResolveItemName, - OnChanged = () => - { - OnListChanged?.Invoke(); - RecalculateLayout(); - ParentTreeListNode?.RefreshLayout(); - }, - }; - AddNode(_itemIdsEditor); - - _namePatternsEditor = new StringListEditorNode - { - Label = "Name Patterns (Regex):", - OnChanged = () => - { - OnListChanged?.Invoke(); - RecalculateLayout(); - ParentTreeListNode?.RefreshLayout(); - }, - }; - AddNode(_namePatternsEditor); - - _uiCategoriesEditor = new UintListEditorNode - { - Label = "UI Categories:", - LabelResolver = CategoryDefinitionConfigurationNode.ResolveUiCategoryName, - OnChanged = () => - { - OnListChanged?.Invoke(); - RecalculateLayout(); - ParentTreeListNode?.RefreshLayout(); - }, - }; - AddNode(_uiCategoriesEditor); - - _raritiesEditor = new RarityEditorNode - { - OnChanged = () => OnValueChanged?.Invoke(), - }; - AddNode(_raritiesEditor); - - RecalculateLayout(); - } - - public void Refresh() - { - EnsureInitialized(); - - _itemIdsEditor!.SetList(CategoryDefinition.Rules.AllowedItemIds); - _namePatternsEditor!.SetList(CategoryDefinition.Rules.AllowedItemNamePatterns); - _uiCategoriesEditor!.SetList(CategoryDefinition.Rules.AllowedUiCategoryIds); - _raritiesEditor!.SetList(CategoryDefinition.Rules.AllowedRarities); - - RecalculateLayout(); - ParentTreeListNode?.RefreshLayout(); - } } \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/CategoryGeneralConfigurationNode.cs b/AetherBags/Nodes/Configuration/Category/CategoryGeneralConfigurationNode.cs index 39038de..3f0fd58 100644 --- a/AetherBags/Nodes/Configuration/Category/CategoryGeneralConfigurationNode.cs +++ b/AetherBags/Nodes/Configuration/Category/CategoryGeneralConfigurationNode.cs @@ -80,24 +80,21 @@ public sealed class CategoryGeneralConfigurationNode : TabbedVerticalListNode bool bisBuddyReady = System.IPC.BisBuddy?.IsReady ?? false; - LabeledDropdownNode? bbModeDropdown = new LabeledDropdownNode + LabeledEnumDropdownNode? bbModeDropdown = new LabeledEnumDropdownNode { - Size = new Vector2(300, 20), + Size = new Vector2(500, 20), LabelText = "Filter Display Mode", LabelTextFlags = TextFlags.AutoAdjustNodeSize, IsEnabled = config.BisBuddyEnabled && bisBuddyReady, - Options = Enum.GetNames(typeof(PluginFilterMode)).ToList(), - SelectedOption = config.BisBuddyMode.ToString(), + Options = Enum.GetValues().ToList(), + SelectedOption = config.BisBuddyMode, OnOptionSelected = selected => { - if (Enum.TryParse(selected, out var parsed)) - { - config.BisBuddyMode = parsed; - if (parsed == PluginFilterMode.Categorize) - HighlightState.ClearFilter(HighlightSource.AllaganTools); + config.BisBuddyMode = selected; + if (selected == PluginFilterMode.Categorize) + HighlightState.ClearFilter(HighlightSource.AllaganTools); - RefreshInventory(); - } + RefreshInventory(); } }; @@ -121,24 +118,23 @@ public sealed class CategoryGeneralConfigurationNode : TabbedVerticalListNode bool allaganReady = System.IPC.AllaganTools?.IsReady ?? false; - LabeledDropdownNode? atModeDropdown = new LabeledDropdownNode + LabeledEnumDropdownNode? atModeDropdown = new LabeledEnumDropdownNode { - Size = new Vector2(300, 20), + Size = new Vector2(500, 20), LabelText = "Filter Display Mode", LabelTextFlags = TextFlags.AutoAdjustNodeSize, IsEnabled = config.AllaganToolsCategoriesEnabled && allaganReady, - Options = Enum.GetNames(typeof(PluginFilterMode)).ToList(), - SelectedOption = config.AllaganToolsFilterMode.ToString(), + Options = Enum.GetValues().ToList(), + SelectedOption = config.AllaganToolsFilterMode, OnOptionSelected = selected => { - if (Enum.TryParse(selected, out var parsed)) + config.AllaganToolsFilterMode = selected; + if (selected == PluginFilterMode.Categorize) { - config.AllaganToolsFilterMode = parsed; - if (parsed == PluginFilterMode.Categorize) - HighlightState.ClearFilter(HighlightSource.AllaganTools); - - RefreshInventory(); + HighlightState.ClearFilter(HighlightSource.AllaganTools); } + + RefreshInventory(); } }; diff --git a/AetherBags/Nodes/Configuration/Category/CategoryScrollingAreaNode.cs b/AetherBags/Nodes/Configuration/Category/CategoryScrollingAreaNode.cs index d6b0ef5..9366a00 100644 --- a/AetherBags/Nodes/Configuration/Category/CategoryScrollingAreaNode.cs +++ b/AetherBags/Nodes/Configuration/Category/CategoryScrollingAreaNode.cs @@ -7,7 +7,6 @@ namespace AetherBags.Nodes.Configuration.Category; public sealed class CategoryScrollingAreaNode : ScrollingListNode { private AddonCategoryConfigurationWindow? _categoryConfigurationAddon; - private readonly TextButtonNode _categoryConfigurationButtonNode; public CategoryScrollingAreaNode() { @@ -15,13 +14,13 @@ public sealed class CategoryScrollingAreaNode : ScrollingListNode AddNode(new CategoryGeneralConfigurationNode()); - _categoryConfigurationButtonNode = new TextButtonNode + var categoryConfigurationButtonNode = new TextButtonNode { Size = new Vector2(300, 28), String = "Configure Categories", OnClick = () => _categoryConfigurationAddon?.Toggle(), }; - AddNode(_categoryConfigurationButtonNode); + AddNode(categoryConfigurationButtonNode); } private void InitializeCategoryAddon() { @@ -33,4 +32,18 @@ public sealed class CategoryScrollingAreaNode : ScrollingListNode Title = "Category Configuration Window", }; } + + protected override void Dispose(bool disposing, bool isNativeDestructor) + { + if (disposing) + { + if (_categoryConfigurationAddon != null) + { + _categoryConfigurationAddon.Close(); + _categoryConfigurationAddon = null; + } + } + + base.Dispose(disposing, isNativeDestructor); + } } \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/ListFiltersSection.cs b/AetherBags/Nodes/Configuration/Category/ListFiltersSection.cs new file mode 100644 index 0000000..76f0f6a --- /dev/null +++ b/AetherBags/Nodes/Configuration/Category/ListFiltersSection.cs @@ -0,0 +1,114 @@ +using System; +using System.Linq; +using AetherBags.Addons; +using AetherBags.Configuration; +using Lumina.Excel.Sheets; +using Action = System.Action; + +namespace AetherBags.Nodes.Configuration.Category; + +public sealed class ListFiltersSection(Func getCategoryDefinition) : ConfigurationSection(getCategoryDefinition) +{ + public Action? OnListChanged { get; init; } + + private UintListEditorNode? _itemIdsEditor; + private StringListEditorNode? _namePatternsEditor; + private UintListEditorNode? _uiCategoriesEditor; + private RarityEditorNode? _raritiesEditor; + + private bool _initialized; + + private AddonItemPicker? _itemPicker; + private AddonUICategoryPicker? _categoryPicker; + + private void EnsureInitialized() + { + if (_initialized) return; + _initialized = true; + + _itemIdsEditor = new UintListEditorNode + { + Label = "Allowed Item IDs:", + LabelResolver = CategoryDefinitionConfigurationNode.ResolveItemName, + OnSearchButtonClicked = OpenItemPicker, + OnChanged = () => + { + OnListChanged?.Invoke(); + RefreshLayout(); + }, + }; + AddNode(_itemIdsEditor); + + _namePatternsEditor = new StringListEditorNode + { + Label = "Name Patterns (Regex):", + OnChanged = () => + { + OnListChanged?.Invoke(); + RefreshLayout(); + }, + }; + AddNode(_namePatternsEditor); + + _uiCategoriesEditor = new UintListEditorNode + { + Label = "UI Categories:", + LabelResolver = CategoryDefinitionConfigurationNode.ResolveUiCategoryName, + OnSearchButtonClicked = OpenCategoryPicker, + OnChanged = () => + { + OnListChanged?.Invoke(); + RefreshLayout(); + }, + }; + AddNode(_uiCategoriesEditor); + + _raritiesEditor = new RarityEditorNode + { + OnChanged = () => OnValueChanged?.Invoke(), + }; + AddNode(_raritiesEditor); + + RecalculateLayout(); + } + + private void OpenItemPicker() { + _itemPicker ??= new AddonItemPicker + { + Title = "Select Items to Add", + InternalName = "Aetherbags_ItemPicker", + SearchOptions = Services.DataManager.GetExcelSheet() + .Where(i => i.RowId > 0 && !i.Name.IsEmpty) + .ToList(), + + SortingOptions = ["Alphabetical", "Id"], + ItemSpacing = 3.0f, + }; + _itemPicker.SelectionResult = item => _itemIdsEditor?.AddValue(item.RowId); + _itemPicker.Open(); + } + + private void OpenCategoryPicker() { + _categoryPicker ??= new AddonUICategoryPicker { + Title = "Select Categories to Add", + InternalName = "Aetherbags_CategoryPicker", + SearchOptions = Services.DataManager.GetExcelSheet() + .Where(i => i.RowId > 0) + .ToList() + }; + _categoryPicker.SelectionResult = cat => _uiCategoriesEditor?.AddValue(cat.RowId); + _categoryPicker.Open(); + } + + public override void Refresh() + { + EnsureInitialized(); + + _itemIdsEditor!.SetList(CategoryDefinition.Rules.AllowedItemIds); + _namePatternsEditor!.SetList(CategoryDefinition.Rules.AllowedItemNamePatterns); + _uiCategoriesEditor!.SetList(CategoryDefinition.Rules.AllowedUiCategoryIds); + _raritiesEditor!.SetList(CategoryDefinition.Rules.AllowedRarities); + + RecalculateLayout(); + } +} \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/RangeFilterRow.cs b/AetherBags/Nodes/Configuration/Category/RangeFilterRow.cs index 9304a42..a7c32df 100644 --- a/AetherBags/Nodes/Configuration/Category/RangeFilterRow.cs +++ b/AetherBags/Nodes/Configuration/Category/RangeFilterRow.cs @@ -3,6 +3,7 @@ using System.Numerics; using AetherBags.Configuration; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Nodes; +using Lumina.Text.ReadOnly; namespace AetherBags.Nodes.Configuration.Category; @@ -14,9 +15,9 @@ public sealed class RangeFilterRow : VerticalListNode public Action? OnFilterChanged { get; set; } - public required string Label + public required ReadOnlySeString Label { - get => _enabledCheckbox.String.Replace(" Filter", ""); + get => _enabledCheckbox.String.ExtractText().Replace(" Filter", ""); init => _enabledCheckbox.String = $"{value} Filter"; } @@ -113,9 +114,9 @@ public sealed class RangeFilterRowUint : VerticalListNode public Action? OnFilterChanged { get; set; } - public required string Label + public required ReadOnlySeString Label { - get => _enabledCheckbox.String.Replace(" Filter", ""); + get => _enabledCheckbox.String.ExtractText().Replace(" Filter", ""); init => _enabledCheckbox.String = $"{value} Filter"; } diff --git a/AetherBags/Nodes/Configuration/Category/RangeFiltersSection.cs b/AetherBags/Nodes/Configuration/Category/RangeFiltersSection.cs new file mode 100644 index 0000000..8b7c1ad --- /dev/null +++ b/AetherBags/Nodes/Configuration/Category/RangeFiltersSection.cs @@ -0,0 +1,77 @@ +using System; +using AetherBags.Configuration; + +namespace AetherBags.Nodes.Configuration.Category; + +public sealed class RangeFiltersSection(Func getCategoryDefinition) : ConfigurationSection(getCategoryDefinition) +{ + private RangeFilterRow? _levelFilter; + private RangeFilterRow? _itemLevelFilter; + private RangeFilterRowUint? _vendorPriceFilter; + + private bool _initialized; + + private void EnsureInitialized() + { + if (_initialized) return; + _initialized = true; + + _levelFilter = new RangeFilterRow + { + Label = "Level", + MinBound = 0, + MaxBound = 200, + OnFilterChanged = (enabled, min, max) => + { + CategoryDefinition.Rules.Level.Enabled = enabled; + CategoryDefinition.Rules.Level.Min = min; + CategoryDefinition.Rules.Level.Max = max; + OnValueChanged?.Invoke(); + }, + }; + AddNode(_levelFilter); + + _itemLevelFilter = new RangeFilterRow + { + Label = "Item Level", + MinBound = 0, + MaxBound = 2000, + OnFilterChanged = (enabled, min, max) => + { + CategoryDefinition.Rules.ItemLevel.Enabled = enabled; + CategoryDefinition.Rules.ItemLevel.Min = min; + CategoryDefinition.Rules.ItemLevel.Max = max; + OnValueChanged?.Invoke(); + }, + }; + AddNode(_itemLevelFilter); + + _vendorPriceFilter = new RangeFilterRowUint + { + Label = "Vendor Price", + MinBound = 0, + MaxBound = 9_999_999, + OnFilterChanged = (enabled, min, max) => + { + CategoryDefinition.Rules.VendorPrice.Enabled = enabled; + CategoryDefinition.Rules.VendorPrice.Min = min; + CategoryDefinition.Rules.VendorPrice.Max = max; + OnValueChanged?.Invoke(); + }, + }; + AddNode(_vendorPriceFilter); + + RecalculateLayout(); + } + + public override void Refresh() + { + EnsureInitialized(); + + _levelFilter!.SetFilter(CategoryDefinition.Rules.Level); + _itemLevelFilter!.SetFilter(CategoryDefinition.Rules.ItemLevel); + _vendorPriceFilter!.SetFilter(CategoryDefinition.Rules.VendorPrice); + + RecalculateLayout(); + } +} \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/StateFiltersSection.cs b/AetherBags/Nodes/Configuration/Category/StateFiltersSection.cs new file mode 100644 index 0000000..b9f9e11 --- /dev/null +++ b/AetherBags/Nodes/Configuration/Category/StateFiltersSection.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using AetherBags.Configuration; + +namespace AetherBags.Nodes.Configuration.Category; + +public sealed class StateFiltersSection(Func getCategoryDefinition) + : ConfigurationSection(getCategoryDefinition) +{ + private readonly List<(StateFilterRowNode Node, Func GetFilter)> _filters = []; + private bool _initialized; + + private void EnsureInitialized() + { + if (_initialized) return; + _initialized = true; + + AddFilter("Untradable", def => def.Rules.Untradable); + AddFilter("Unique", def => def.Rules.Unique); + AddFilter("Collectable", def => def.Rules.Collectable); + AddFilter("Dyeable", def => def.Rules.Dyeable); + AddFilter("Repairable", def => def.Rules.Repairable); + AddFilter("High Quality", def => def.Rules.HighQuality); + AddFilter("Desynthesizable", def => def.Rules.Desynthesizable); + AddFilter("Glamourable", def => def.Rules.Glamourable); + AddFilter("Spiritbonded", def => def.Rules.FullySpiritbonded); + + RecalculateLayout(); + } + + private void AddFilter(string label, Func getFilter) + { + var node = new StateFilterRowNode(label, new StateFilter(), () => OnValueChanged?.Invoke()); + _filters.Add((node, getFilter)); + AddNode(node); + } + + public override void Refresh() + { + EnsureInitialized(); + + foreach (var (node, getFilter) in _filters) + { + node.SetState(getFilter(CategoryDefinition)); + } + + RecalculateLayout(); + } +} \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/StringListEditorNode.cs b/AetherBags/Nodes/Configuration/Category/StringListEditorNode.cs index 5cbcd65..6238a44 100644 --- a/AetherBags/Nodes/Configuration/Category/StringListEditorNode.cs +++ b/AetherBags/Nodes/Configuration/Category/StringListEditorNode.cs @@ -4,6 +4,7 @@ using System.Numerics; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Classes; using KamiToolKit.Nodes; +using Lumina.Text.ReadOnly; namespace AetherBags.Nodes.Configuration.Category; @@ -16,12 +17,11 @@ public sealed class StringListEditorNode : VerticalListNode private readonly LabelTextNode _headerLabel; private readonly VerticalListNode _itemsContainer; - private readonly HorizontalListNode _addRow; private readonly TextInputNode _addInput; public Action? OnChanged { get; set; } - public required string Label + public required ReadOnlySeString Label { get => _headerLabel.String; init => _headerLabel.String = value; @@ -49,7 +49,7 @@ public sealed class StringListEditorNode : VerticalListNode }; AddNode(_itemsContainer); - _addRow = new HorizontalListNode + var addRow = new HorizontalListNode { Size = new Vector2(LabelWidth + 40f, RowHeight), ItemSpacing = 4.0f, @@ -61,7 +61,7 @@ public sealed class StringListEditorNode : VerticalListNode PlaceholderString = "Add new...", OnInputComplete = _ => AddCurrentValue(), }; - _addRow.AddNode(_addInput); + addRow.AddNode(_addInput); var addButton = new TextButtonNode { @@ -69,9 +69,9 @@ public sealed class StringListEditorNode : VerticalListNode String = "Add", OnClick = AddCurrentValue, }; - _addRow.AddNode(addButton); + addRow.AddNode(addButton); - AddNode(_addRow); + AddNode(addRow); } public void SetList(List newList) @@ -82,7 +82,7 @@ public sealed class StringListEditorNode : VerticalListNode private void AddCurrentValue() { - var value = _addInput.String; + var value = _addInput.String.ExtractText(); if (!string.IsNullOrWhiteSpace(value) && !_list.Contains(value)) { _list.Add(value); diff --git a/AetherBags/Nodes/Configuration/Category/UICategoryListItemNode.cs b/AetherBags/Nodes/Configuration/Category/UICategoryListItemNode.cs new file mode 100644 index 0000000..a87dc47 --- /dev/null +++ b/AetherBags/Nodes/Configuration/Category/UICategoryListItemNode.cs @@ -0,0 +1,31 @@ +using System.Numerics; +using FFXIVClientStructs.FFXIV.Component.GUI; +using KamiToolKit.Classes; +using KamiToolKit.Nodes; +using Lumina.Excel.Sheets; + +namespace AetherBags.Nodes.Configuration.Category; + +public class UICategoryListItemNode : ListItemNode { + public override float ItemHeight => 30.0f; + protected readonly TextNode LabelTextNode; + + public UICategoryListItemNode() { + LabelTextNode = new TextNode { + FontSize = 14, + AlignmentType = AlignmentType.Left, + TextColor = ColorHelper.GetColor(8), + }; + LabelTextNode.AttachNode(this); + } + + protected override void OnSizeChanged() { + base.OnSizeChanged(); + LabelTextNode.Size = Size with { X = Width - 10 }; + LabelTextNode.Position = new Vector2(5, 0); + } + + protected override void SetNodeData(ItemUICategory data) { + LabelTextNode.String = data.Name.ToString(); + } +} \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/UintListEditorNode.cs b/AetherBags/Nodes/Configuration/Category/UintListEditorNode.cs index 458ab75..2544f50 100644 --- a/AetherBags/Nodes/Configuration/Category/UintListEditorNode.cs +++ b/AetherBags/Nodes/Configuration/Category/UintListEditorNode.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Numerics; +using AetherBags.Configuration; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Classes; using KamiToolKit.Nodes; +using Lumina.Text.ReadOnly; namespace AetherBags.Nodes.Configuration.Category; @@ -14,15 +17,18 @@ public sealed class UintListEditorNode : VerticalListNode private List _list = []; + public List GetList() => _list.ToList(); + private readonly LabelTextNode _headerLabel; private readonly VerticalListNode _itemsContainer; - private readonly HorizontalListNode _addRow; private readonly NumericInputNode _addInput; + public Action? OnSearchButtonClicked { get; init; } + public Func? LabelResolver { get; init; } public Action? OnChanged { get; set; } - public required string Label + public required ReadOnlySeString Label { get => _headerLabel.String; init => _headerLabel.String = value; @@ -50,12 +56,21 @@ public sealed class UintListEditorNode : VerticalListNode }; AddNode(_itemsContainer); - _addRow = new HorizontalListNode + var addRow = new HorizontalListNode { Size = new Vector2(LabelWidth + 40f, RowHeight), ItemSpacing = 4.0f, }; + var searchButton = new CircleButtonNode + { + Size = new Vector2(28), + Icon = ButtonIcon.MagnifyingGlass, + OnClick = () => OnSearchButtonClicked?.Invoke(), + TextTooltip = "Search the game database..." + }; + addRow.AddNode(searchButton); + _addInput = new NumericInputNode { Size = new Vector2(120, RowHeight), @@ -63,7 +78,7 @@ public sealed class UintListEditorNode : VerticalListNode Max = int.MaxValue, Value = 0, }; - _addRow.AddNode(_addInput); + addRow.AddNode(_addInput); var addButton = new TextButtonNode { @@ -71,9 +86,10 @@ public sealed class UintListEditorNode : VerticalListNode String = "Add", OnClick = AddCurrentValue, }; - _addRow.AddNode(addButton); - - AddNode(_addRow); + addRow.AddNode(addButton); + addRow.RecalculateLayout(); + AddNode(addRow); + RecalculateLayout(); } public void SetList(List newList) @@ -82,6 +98,16 @@ public sealed class UintListEditorNode : VerticalListNode RefreshItems(); } + public void AddValue(uint value) + { + if (!_list.Contains(value)) + { + _list.Add(value); + RefreshItems(); + OnChanged?.Invoke(); + } + } + private void AddCurrentValue() { var value = (uint)_addInput.Value; @@ -109,6 +135,7 @@ public sealed class UintListEditorNode : VerticalListNode _itemsContainer.RecalculateLayout(); RecalculateLayout(); + OnChanged?.Invoke(); } private UintListItemNode CreateItemNode(uint value) => new(value, LabelResolver) @@ -120,8 +147,10 @@ public sealed class UintListEditorNode : VerticalListNode private void RemoveValue(uint value) { _list.Remove(value); - RefreshItems(); - OnChanged?.Invoke(); + Services.Framework.RunOnTick(() => { + RefreshItems(); + OnChanged?.Invoke(); + }); } } @@ -137,9 +166,15 @@ public sealed class UintListItemNode : HorizontalListNode Value = value; ItemSpacing = 4.0f; + string idDisplay = value switch { + 0xFFFF_FFFE => "[Weekly]", + 0xFFFF_FFFD => "[Tome]", + _ => value.ToString() + }; + var displayText = labelResolver is not null - ? $"{value} - {labelResolver(value)}" - : value.ToString(); + ? $"{idDisplay} - {labelResolver(value)}" + : idDisplay; AddNode(new LabelTextNode { diff --git a/AetherBags/Nodes/Configuration/Currency/CurrencyGeneralConfigurationNode.cs b/AetherBags/Nodes/Configuration/Currency/CurrencyGeneralConfigurationNode.cs index e709455..412eab4 100644 --- a/AetherBags/Nodes/Configuration/Currency/CurrencyGeneralConfigurationNode.cs +++ b/AetherBags/Nodes/Configuration/Currency/CurrencyGeneralConfigurationNode.cs @@ -1,22 +1,29 @@ +using System; using System.Numerics; +using AetherBags.Addons; using AetherBags.Configuration; using AetherBags.Nodes.Color; +using AetherBags.Nodes.Configuration.Category; using KamiToolKit.Classes; using KamiToolKit.Nodes; +using Lumina.Excel.Sheets; namespace AetherBags.Nodes.Configuration.Currency; public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode { + private readonly UintListEditorNode? _currencyListEditor; + public CurrencyGeneralConfigurationNode() { CurrencySettings config = System.Config.Currency; + Width = 600; ItemVerticalSpacing = 2; LabelTextNode titleNode = new LabelTextNode { - Size = Size with { Y = 18 }, + Size = new Vector2(Width, 18), String = "Currency Configuration", TextColor = ColorHelper.GetColor(2), TextOutlineColor = ColorHelper.GetColor(0), @@ -27,7 +34,7 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode CheckboxNode currencyEnabledCheckbox = new CheckboxNode { - Size = Size with { Y = 18 }, + Size = new Vector2(Width, 18), IsVisible = true, String = "Show Currency", IsChecked = config.Enabled, @@ -41,21 +48,23 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode AddTab(1); + var defaultColorHandler = CreateColorHandler(color => config.DefaultColor = color); ColorInputRow defaultCurrencyColorNode = new ColorInputRow { Label = "Default Currency Color", Size = new Vector2(300, 24), CurrentColor = config.DefaultColor, DefaultColor = new CurrencySettings().DefaultColor, - OnColorConfirmed = ApplyColorChange, - OnColorChange = ApplyColorChange, - OnColorCanceled = ApplyColorChange, + OnColorConfirmed = defaultColorHandler, + OnColorChange = defaultColorHandler, + OnColorCanceled = defaultColorHandler, + OnColorPreviewed = defaultColorHandler, }; AddNode(defaultCurrencyColorNode); CheckboxNode cappedEnabledCheckbox = new CheckboxNode { - Size = Size with { Y = 18 }, + Size = new Vector2(Width, 18), IsVisible = true, String = "Color Weekly Cap", IsChecked = config.ColorWhenCapped, @@ -70,18 +79,17 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode AddTab(1); - + var cappedColorHandler = CreateColorHandler(color => config.CappedColor = color); ColorInputRow cappedCurrencyColorNode = new ColorInputRow { Label = "Weekly Cap Color", Size = new Vector2(300, 24), CurrentColor = config.CappedColor, DefaultColor = new CurrencySettings().CappedColor, - OnColorConfirmed = color => - { - config.CappedColor = color; - RefreshCurrency(); - }, + OnColorConfirmed = cappedColorHandler, + OnColorChange = cappedColorHandler, + OnColorCanceled = cappedColorHandler, + OnColorPreviewed = cappedColorHandler, }; AddNode(cappedCurrencyColorNode); @@ -89,7 +97,7 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode CheckboxNode limitedEnabledCheckbox = new CheckboxNode { - Size = Size with { Y = 18 }, + Size = new Vector2(Width, 18), IsVisible = true, String = "Color Max Capacity", IsChecked = config.ColorWhenLimited, @@ -104,28 +112,83 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode AddTab(1); + var limitColorHandler = CreateColorHandler(color => config.LimitColor = color); ColorInputRow limitCurrencyColorNode = new ColorInputRow { Label = "Max Capacity Color", Size = new Vector2(300, 24), CurrentColor = config.LimitColor, DefaultColor = new CurrencySettings().LimitColor, - OnColorConfirmed = color => - { - config.LimitColor = color; - RefreshCurrency(); - }, + OnColorConfirmed = limitColorHandler, + OnColorChange = limitColorHandler, + OnColorCanceled = limitColorHandler, + OnColorPreviewed = limitColorHandler, }; AddNode(limitCurrencyColorNode); - return; + AddNode(new ResNode { Size = new Vector2(15) }); - void ApplyColorChange(Vector4 color) + SubtractTab(2); + + AddNode(new ResNode { Size = new Vector2(15) }); + + _currencyListEditor = new UintListEditorNode { - config.DefaultColor = color; - RefreshCurrency(); - } + Label = "Displayed Currencies:", + LabelResolver = id => + { + return id switch + { + CurrencySettings.LimitedTomestoneId => "Current Limited Tomestone", + CurrencySettings.NonLimitedTomestoneId => "Current Non-Limited Tomestone", + _ => Services.DataManager.GetExcelSheet().GetRow(id).Name.ToString() + }; + }, + OnSearchButtonClicked = OpenCurrencyPicker, + OnChanged = () => { + System.Config.Currency.DisplayedCurrencies = _currencyListEditor!.GetList(); + RefreshCurrency(); + RecalculateLayout(); + } + }; + _currencyListEditor.SetList(System.Config.Currency.DisplayedCurrencies); + AddNode(_currencyListEditor); + + var quickAddRow = new HorizontalListNode { Size = new Vector2(600, 30), ItemSpacing = 8.0f }; + + quickAddRow.AddNode(new TextButtonNode { + String = "+ Gil", Size = new Vector2(70, 24), + OnClick = () => _currencyListEditor?.AddValue(1) + }); + + quickAddRow.AddNode(new TextButtonNode { + String = "+ Limited Tomestone", Size = new Vector2(150, 24), + OnClick = () => _currencyListEditor?.AddValue(CurrencySettings.LimitedTomestoneId) + }); + + quickAddRow.AddNode(new TextButtonNode { + String = "+ Non-Limited", Size = new Vector2(110, 24), + OnClick = () => _currencyListEditor?.AddValue(CurrencySettings.NonLimitedTomestoneId) + }); + AddNode(quickAddRow); + RecalculateLayout(); } + private Action CreateColorHandler(Action setter) => newColor => + { + setter(newColor); + RefreshCurrency(); + }; + private void RefreshCurrency() => System.AddonInventoryWindow.ManualCurrencyRefresh(); + + private void OpenCurrencyPicker() { + var picker = new AddonCurrencyPicker + { + Title = "Select Currency to Add", + InternalName = "AetherBags_CurrencyPicker", + }; + picker.SelectionResult = item => _currencyListEditor?.AddValue(item.RowId); + picker.Open(); + } } \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Currency/CurrencyScrollingAreaNode.cs b/AetherBags/Nodes/Configuration/Currency/CurrencyScrollingAreaNode.cs index e8188e0..f95e1e9 100644 --- a/AetherBags/Nodes/Configuration/Currency/CurrencyScrollingAreaNode.cs +++ b/AetherBags/Nodes/Configuration/Currency/CurrencyScrollingAreaNode.cs @@ -1,3 +1,4 @@ +using System.Numerics; using KamiToolKit.Nodes; namespace AetherBags.Nodes.Configuration.Currency; @@ -8,7 +9,7 @@ public sealed class CurrencyScrollingAreaNode : ScrollingListNode { AddNode(new CurrencyGeneralConfigurationNode { - Size = Size + Width = 600 }); } } \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/General/FunctionalConfigurationNode.cs b/AetherBags/Nodes/Configuration/General/FunctionalConfigurationNode.cs index 825af76..877d183 100644 --- a/AetherBags/Nodes/Configuration/General/FunctionalConfigurationNode.cs +++ b/AetherBags/Nodes/Configuration/General/FunctionalConfigurationNode.cs @@ -14,7 +14,7 @@ internal sealed class FunctionalConfigurationNode : TabbedVerticalListNode private readonly CheckboxNode _hideDefaultBagsCheckboxNode; private readonly CheckboxNode _hideSaddlebagsCheckboxNode; private readonly CheckboxNode _hideRetainerbagsCheckboxNode; - private readonly LabeledDropdownNode _stackDropDown; + private readonly LabeledEnumDropdownNode _stackDropDown; public FunctionalConfigurationNode() { @@ -139,39 +139,33 @@ internal sealed class FunctionalConfigurationNode : TabbedVerticalListNode Height = 6 }); - var searchModeDropDown = new LabeledDropdownNode + var searchModeDropDown = new LabeledEnumDropdownNode { - Size = new Vector2(300, 20), + Size = new Vector2(500, 20), LabelText = "Search Mode", LabelTextFlags = TextFlags.AutoAdjustNodeSize, - Options = Enum.GetNames(typeof(SearchMode)).ToList(), - SelectedOption = config.SearchMode.ToString(), + Options = Enum.GetValues().ToList(), + SelectedOption = config.SearchMode, OnOptionSelected = selected => { - if (Enum.TryParse(selected, out var parsed)) - { - config.SearchMode = parsed; - InventoryOrchestrator.RefreshAll(updateMaps: false); - } + config.SearchMode = selected; + InventoryOrchestrator.RefreshAll(updateMaps: false); } }; AddNode(searchModeDropDown); - _stackDropDown = new LabeledDropdownNode + _stackDropDown = new LabeledEnumDropdownNode { - Size = new Vector2(300, 20), + Size = new Vector2(500, 20), IsEnabled = true, LabelText = "Stack Mode", LabelTextFlags = TextFlags.AutoAdjustNodeSize, - Options = Enum.GetNames(typeof(InventoryStackMode)).ToList(), - SelectedOption = config.StackMode.ToString(), + Options = Enum.GetValues().ToList(), + SelectedOption = config.StackMode, OnOptionSelected = selected => { - if (Enum.TryParse(selected, out var parsed)) - { - config.StackMode = parsed; - InventoryOrchestrator.RefreshAll(updateMaps: true); - } + config.StackMode = selected; + InventoryOrchestrator.RefreshAll(updateMaps: true); } }; AddNode(_stackDropDown); diff --git a/AetherBags/Nodes/Configuration/General/ImportExportResetNode.cs b/AetherBags/Nodes/Configuration/General/ImportExportResetNode.cs index ed9ab36..cac1e17 100644 --- a/AetherBags/Nodes/Configuration/General/ImportExportResetNode.cs +++ b/AetherBags/Nodes/Configuration/General/ImportExportResetNode.cs @@ -3,6 +3,7 @@ using AetherBags.Helpers; using AetherBags.Inventory; using Dalamud.Game.ClientState.Keys; using KamiToolKit.Classes; +using KamiToolKit.Enums; using KamiToolKit.Nodes; namespace AetherBags.Nodes.Configuration.General; diff --git a/AetherBags/Nodes/Configuration/Layout/CompactLookaheadNode.cs b/AetherBags/Nodes/Configuration/Layout/CompactLookaheadNode.cs index 5c2e597..a98b783 100644 --- a/AetherBags/Nodes/Configuration/Layout/CompactLookaheadNode.cs +++ b/AetherBags/Nodes/Configuration/Layout/CompactLookaheadNode.cs @@ -1,9 +1,9 @@ using AetherBags.Configuration; -using KamiToolKit.Classes.Timelines; using KamiToolKit.Nodes; using System.Numerics; using AetherBags.Inventory; using FFXIVClientStructs.FFXIV.Component.GUI; +using KamiToolKit.Timelines; namespace AetherBags.Nodes.Configuration.Layout; diff --git a/AetherBags/Nodes/Input/LabeledDropdownNode.cs b/AetherBags/Nodes/Input/LabeledEnumDropdownNode.cs similarity index 57% rename from AetherBags/Nodes/Input/LabeledDropdownNode.cs rename to AetherBags/Nodes/Input/LabeledEnumDropdownNode.cs index e2149c5..83fd449 100644 --- a/AetherBags/Nodes/Input/LabeledDropdownNode.cs +++ b/AetherBags/Nodes/Input/LabeledEnumDropdownNode.cs @@ -2,27 +2,28 @@ using System; using System.Collections.Generic; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Nodes; +using Lumina.Text.ReadOnly; namespace AetherBags.Nodes.Input; -public class LabeledDropdownNode : SimpleComponentNode { +public class LabeledEnumDropdownNode : SimpleComponentNode where T : Enum { private readonly GridNode _gridNode; private readonly TextNode _labelNode; - private readonly TextDropDownNode _dropDownNode; + private readonly EnumDropDownNode _dropDownNode; - public LabeledDropdownNode() { + public LabeledEnumDropdownNode() { _gridNode = new GridNode { GridSize = new GridSize(2, 1), }; _gridNode.AttachNode(this); _labelNode = new LabelTextNode { - String = String.Empty, + String = string.Empty, }; _labelNode.AttachNode(_gridNode[0, 0]); - _dropDownNode = new TextDropDownNode { - Options = new List(), + _dropDownNode = new EnumDropDownNode { + Options = new List(), }; _dropDownNode.AttachNode(_gridNode[1, 0]); } @@ -36,25 +37,38 @@ public class LabeledDropdownNode : SimpleComponentNode { _dropDownNode.Size = _gridNode[1, 0].Size; } - public required string LabelText + public required ReadOnlySeString LabelText { get => _labelNode.String; set => _labelNode.String = value; } - public Action? OnOptionSelected + public Action? OnOptionSelected { get => _dropDownNode.OnOptionSelected; set => _dropDownNode.OnOptionSelected = value; } - public string? SelectedOption + public T? SelectedOption { - get => _dropDownNode.SelectedOption; - set => _dropDownNode.SelectedOption = value; + get => _dropDownNode.OptionListNode.SelectedOption; + set + { + _dropDownNode.OptionListNode.SelectedOption = value; + if (value != null) + { + _dropDownNode.LabelNode.String = value.Description; + } + } } - public required List Options + public int MaxListOptions + { + get => _dropDownNode.MaxListOptions; + set => _dropDownNode.MaxListOptions = value; + } + + public required List Options { get => _dropDownNode.Options!; set => _dropDownNode.Options = value; @@ -65,4 +79,4 @@ public class LabeledDropdownNode : SimpleComponentNode { get => _labelNode.TextFlags; set => _labelNode.TextFlags = value; } -} +} \ No newline at end of file diff --git a/AetherBags/Nodes/Input/TextInputWithButtonNode.cs b/AetherBags/Nodes/Input/TextInputWithButtonNode.cs index 8b9d56f..8136840 100644 --- a/AetherBags/Nodes/Input/TextInputWithButtonNode.cs +++ b/AetherBags/Nodes/Input/TextInputWithButtonNode.cs @@ -49,7 +49,7 @@ public class TextInputWithButtonNode : SimpleComponentNode { } public ReadOnlySeString SearchString { - get => _textInputNode.SeString; - set => _textInputNode.SeString = value; + get => _textInputNode.String; + set => _textInputNode.String = value; } } \ No newline at end of file diff --git a/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs index e1af79d..0a26458 100644 --- a/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs +++ b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs @@ -61,7 +61,7 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase _categoryNameTextNode.TextFlags |= TextFlags.OverflowHidden | TextFlags.Ellipsis; _categoryNameTextNode.TextFlags &= ~(TextFlags.WordWrap | TextFlags.MultiLine); - _categoryNameTextNode.AddFlags(NodeFlags.EmitsEvents | NodeFlags.HasCollision); + _categoryNameTextNode.AddNodeFlags(NodeFlags.EmitsEvents | NodeFlags.HasCollision); _categoryNameTextNode.AttachNode(this); _itemGridNode = new HybridDirectionalFlexNode diff --git a/AetherBags/Nodes/Inventory/InventoryDragDropNode.cs b/AetherBags/Nodes/Inventory/InventoryDragDropNode.cs index c14a1fd..b5feefe 100644 --- a/AetherBags/Nodes/Inventory/InventoryDragDropNode.cs +++ b/AetherBags/Nodes/Inventory/InventoryDragDropNode.cs @@ -18,7 +18,7 @@ public class InventoryDragDropNode : DragDropNode Size = new Vector2(40.0f, 12.0f), Position = new Vector2(4.0f, 34.0f), NodeFlags = NodeFlags.Enabled | NodeFlags.EmitsEvents, - Color = ColorHelper.GetColor(50), + TextColor = ColorHelper.GetColor(50), TextOutlineColor = ColorHelper.GetColor(51), TextFlags = TextFlags.Edge, AlignmentType = AlignmentType.Right, diff --git a/AetherBags/Nodes/Inventory/InventoryFooterNode.cs b/AetherBags/Nodes/Inventory/InventoryFooterNode.cs index f05aa2f..436c904 100644 --- a/AetherBags/Nodes/Inventory/InventoryFooterNode.cs +++ b/AetherBags/Nodes/Inventory/InventoryFooterNode.cs @@ -5,7 +5,7 @@ using AetherBags.Nodes.Currency; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Classes; using KamiToolKit.Nodes; - +using Lumina.Text.ReadOnly; using static AetherBags.Inventory.State.InventoryStateBase; namespace AetherBags.Nodes.Inventory; @@ -33,7 +33,8 @@ public sealed class InventoryFooterNode : SimpleComponentNode { Position = new Vector2(0, 0), Size = new Vector2(120, 28), - IsVisible = System.Config.Currency.Enabled + IsVisible = System.Config.Currency.Enabled, + ItemSpacing = 12f, }; _currencyListNode.AttachNode(this); @@ -42,9 +43,13 @@ public sealed class InventoryFooterNode : SimpleComponentNode public void RefreshCurrencies() { - _currencyListNode.IsVisible = System.Config.Currency.Enabled; + var config = System.Config.Currency; + _currencyListNode.IsVisible = config.Enabled; - IReadOnlyList currencyInfoList = GetCurrencyInfoList([1, 28, 0xFFFF_FFFE, 0xFFFF_FFFD]); + if (!config.Enabled) return; + + //IReadOnlyList currencyInfoList = GetCurrencyInfoList([1, 28, 0xFFFF_FFFE, 0xFFFF_FFFD]); + IReadOnlyList currencyInfoList = GetCurrencyInfoList(config.DisplayedCurrencies.ToArray()); _currencyListNode.SyncWithListDataByKey( dataList: currencyInfoList, getKeyFromData: currencyInfo => currencyInfo.ItemId, @@ -60,7 +65,7 @@ public sealed class InventoryFooterNode : SimpleComponentNode }); } - public string SlotAmountText + public ReadOnlySeString SlotAmountText { get => _slotAmountTextNode.String; set => _slotAmountTextNode.String = value; diff --git a/AetherBags/Nodes/Inventory/InventoryNotificationNode.cs b/AetherBags/Nodes/Inventory/InventoryNotificationNode.cs index 8b26f29..786b4c0 100644 --- a/AetherBags/Nodes/Inventory/InventoryNotificationNode.cs +++ b/AetherBags/Nodes/Inventory/InventoryNotificationNode.cs @@ -2,8 +2,8 @@ using System.Numerics; using AetherBags.Inventory.Context; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Classes; -using KamiToolKit.Classes.Timelines; using KamiToolKit.Nodes; +using KamiToolKit.Timelines; namespace AetherBags.Nodes.Inventory; @@ -13,8 +13,6 @@ public sealed class InventoryNotificationNode : SimpleComponentNode private readonly TextNode titleTextNode; private readonly TextNode messageTextNode; - private static readonly InventoryNotificationState NotificationState = new(); - public InventoryNotificationNode() { AddTimeline(ParentLabels); @@ -76,8 +74,8 @@ public sealed class InventoryNotificationNode : SimpleComponentNode { field = value; - titleTextNode.SeString = value.Title; - messageTextNode.SeString = value.Message; + titleTextNode.String = value.Title; + messageTextNode.String = value.Message; if (value.Title.IsEmpty && value.Message.IsEmpty) { diff --git a/AetherBags/Nodes/Inventory/LootedItemsCategoryNode.cs b/AetherBags/Nodes/Inventory/LootedItemsCategoryNode.cs index 99f6688..cd55089 100644 --- a/AetherBags/Nodes/Inventory/LootedItemsCategoryNode.cs +++ b/AetherBags/Nodes/Inventory/LootedItemsCategoryNode.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using AetherBags.Inventory.Items; using AetherBags.Nodes.Layout; using FFXIVClientStructs.FFXIV.Component.GUI; +using KamiToolKit.Classes; using KamiToolKit.Nodes; namespace AetherBags.Nodes.Inventory; @@ -62,7 +63,7 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase AlignmentType = AlignmentType.Left, String = "Recently Looted", TextFlags = TextFlags.OverflowHidden | TextFlags.Ellipsis, - TextColor = new Vector4(0.9f, 0.8f, 0.5f, 1.0f), // Gold-ish color + TextColor = ColorHelper.GetColor(26), // Gold-ish color }; _headerTextNode.AddEvent(AtkEventType.MouseOver, BeginHeaderHover); @@ -71,7 +72,7 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase _headerTextNode.TextFlags |= TextFlags.OverflowHidden | TextFlags.Ellipsis; _headerTextNode.TextFlags &= ~(TextFlags.WordWrap | TextFlags.MultiLine); - _headerTextNode.AddFlags(NodeFlags.EmitsEvents | NodeFlags.HasCollision); + _headerTextNode.AddNodeFlags(NodeFlags.EmitsEvents | NodeFlags.HasCollision); _headerTextNode.AttachNode(this); _clearButton = new CircleButtonNode @@ -197,7 +198,7 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase private void SyncItemGrid() { - _itemGridNode.SyncWithListDataByKey( + _itemGridNode.SyncWithListDataByKey( dataList: _lootedItems, getKeyFromData: item => item.Index, getKeyFromNode: node => node.LootedItem?.Index ?? -1, @@ -221,6 +222,7 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase private void OnItemDismissed(LootedItemDisplayNode node) { + if(node.LootedItem is null) return; int index = node.LootedItem.Index; OnDismissItem?.Invoke(index); } diff --git a/AetherBags/Nodes/Inventory/SaddleBagFooterNode.cs b/AetherBags/Nodes/Inventory/SaddleBagFooterNode.cs index e3f078e..505bc05 100644 --- a/AetherBags/Nodes/Inventory/SaddleBagFooterNode.cs +++ b/AetherBags/Nodes/Inventory/SaddleBagFooterNode.cs @@ -1,6 +1,7 @@ using System. Numerics; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Nodes; +using Lumina.Text.ReadOnly; namespace AetherBags.Nodes.Inventory; @@ -23,7 +24,7 @@ public class SaddleBagFooterNode : SimpleComponentNode _slotCounterNode.AttachNode(this); } - public string SlotAmountText + public ReadOnlySeString SlotAmountText { get => _slotCounterNode.String; set => _slotCounterNode.String = $"Slots: {value}"; diff --git a/AetherBags/Nodes/Layout/CollapsibleSectionNode.cs b/AetherBags/Nodes/Layout/CollapsibleSectionNode.cs new file mode 100644 index 0000000..2d73abe --- /dev/null +++ b/AetherBags/Nodes/Layout/CollapsibleSectionNode.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using FFXIVClientStructs.FFXIV.Component.GUI; +using KamiToolKit; +using KamiToolKit.Classes; +using KamiToolKit.Nodes; +using Lumina.Text.ReadOnly; + +namespace AetherBags.Nodes.Layout; + +public class CollapsibleSectionNode : VerticalListNode +{ + protected readonly NineGridNode BackgroundNode; + protected readonly ImageNode ArrowNode; + protected readonly TextNode LabelNode; + protected new readonly CollisionNode CollisionNode; + protected readonly TabbedVerticalListNode ContentNode; + protected readonly SimpleComponentNode HeaderNode; + + private bool isCollapsed = true; + private float headerHeight = 28.0f; + + public Action? OnToggle; + + public TabbedVerticalListNode CollapsibleContent => ContentNode; + + public bool IsCollapsed + { + get => isCollapsed; + set { isCollapsed = value; UpdateState(); } + } + + public float HeaderHeight + { + get => headerHeight; + set + { + headerHeight = value; + HeaderNode.Height = value; + BackgroundNode.Height = value; + CollisionNode.Height = value; + ArrowNode.Y = (value - ArrowNode.Height) / 2.0f; + LabelNode.Height = value; + RecalculateLayout(); + } + } + + public uint FontSize { get => LabelNode.FontSize; set => LabelNode.FontSize = value; } + + public float TabSize + { + get => ContentNode.TabSize; + set => ContentNode.TabSize = value; + } + + public int TabStep + { + get => ContentNode.TabStep; + set => ContentNode.TabStep = value; + } + + public bool FitChildWidth + { + get => ContentNode.FitWidth; + set => ContentNode.FitWidth = value; + } + + public float NestingIndent + { + get; + set + { + field = value; + ArrowNode.X = value + 4.0f; + LabelNode.X = value + 23.0f; + ContentNode.X = value + 10.0f; + } + } + + public CollapsibleSectionNode() + { + FitContents = true; + ItemSpacing = 0.0f; + + HeaderNode = new SimpleComponentNode + { + Size = new Vector2(Width, headerHeight) + }; + + BackgroundNode = new SimpleNineGridNode { + TexturePath = "ui/uld/ListItemB.tex", + TextureSize = new Vector2(48.0f, 28.0f), + TextureCoordinates = new Vector2(0.0f, 24.0f), + Size = new Vector2(Width, headerHeight), + TopOffset = 10, LeftOffset = 12, RightOffset = 12, BottomOffset = 12, + Color = new Vector4(0.9f, 0.9f, 0.9f, 1.0f) + }; + BackgroundNode.AttachNode(HeaderNode); + + ArrowNode = new ImageNode { Position = new Vector2(4.0f, 2.0f), Size = new Vector2(24.0f, 24.0f) }; + ArrowNode.AddPart( + new Part { TexturePath = "ui/uld/ListItemB.tex", TextureCoordinates = new Vector2(0, 0), Size = new Vector2(24, 24), Id = 0 }, + new Part { TexturePath = "ui/uld/ListItemB.tex", TextureCoordinates = new Vector2(24, 0), Size = new Vector2(24, 24), Id = 1 } + ); + ArrowNode.AttachNode(HeaderNode); + + LabelNode = new TextNode { + Position = new Vector2(30.0f, 0.0f), + Size = new Vector2(Width - 23, headerHeight), + FontSize = 12, + FontType = FontType.Axis, + AlignmentType = AlignmentType.Left, + TextColor = ColorHelper.GetColor(50), + }; + LabelNode.AttachNode(HeaderNode); + + CollisionNode = new CollisionNode + { + Size = new Vector2(Width, headerHeight), + ShowClickableCursor = true + }; + CollisionNode.AddEvent(AtkEventType.MouseClick, () => { + IsCollapsed = !IsCollapsed; + OnToggle?.Invoke(); + }); + CollisionNode.AttachNode(HeaderNode); + + ContentNode = new TabbedVerticalListNode { + IsVisible = false, + X = 18.0f, + ItemVerticalSpacing = 4.0f, + TabSize = 18.0f, + FitWidth = true, + }; + + base.AddNode([HeaderNode, ContentNode]); + UpdateState(); + } + + public void RefreshLayout() + { + ContentNode.RecalculateLayout(); + RecalculateLayout(); + OnToggle?.Invoke(); + } + + private void UpdateState() + { + ContentNode.IsVisible = !isCollapsed; + ArrowNode.PartId = isCollapsed ? 0u : 1u; + + if (!isCollapsed) + { + ContentNode.Width = Math.Max(0, Width - ContentNode.X); + ContentNode.RecalculateLayout(); + } + + RecalculateLayout(); + OnToggle?.Invoke(); + } + + public void AddTab(int tabAmount = 1) => ContentNode.AddTab(tabAmount); + + public void SubtractTab(int tabAmount = 1) => ContentNode.SubtractTab(tabAmount); + + public new void AddNode(NodeBase node) => ContentNode.AddNode(node); + + public new void AddNode(IEnumerable nodes) => ContentNode.AddNode(nodes); + + public void AddNode(int tabIndex, NodeBase node) => ContentNode.AddNode(tabIndex, node); + + public void AddNode(int tabIndex, IEnumerable nodes) => ContentNode.AddNode(tabIndex, nodes); + + public new void RemoveNode(NodeBase node) => ContentNode.RemoveNode(node); + + public new void Clear() => ContentNode.Clear(); + + protected override void OnSizeChanged() + { + base.OnSizeChanged(); + if (BackgroundNode == null || LabelNode == null || CollisionNode == null) return; + + HeaderNode.Width = Width; + BackgroundNode.Width = Width; + LabelNode.Width = Math.Max(0, Width - LabelNode.X); + CollisionNode.Width = Width; + ContentNode.Width = Math.Max(0, Width - ContentNode.X); + } + + public ReadOnlySeString String { get => LabelNode.String; set => LabelNode.String = value; } +} diff --git a/AetherBags/Nodes/Layout/DeferrableLayoutListNode.cs b/AetherBags/Nodes/Layout/DeferrableLayoutListNode.cs index 3c5f43d..12da585 100644 --- a/AetherBags/Nodes/Layout/DeferrableLayoutListNode.cs +++ b/AetherBags/Nodes/Layout/DeferrableLayoutListNode.cs @@ -49,9 +49,9 @@ public abstract class DeferrableLayoutListNode : SimpleComponentNode set { if (value) - AddFlags(NodeFlags.Clip); + AddNodeFlags(NodeFlags.Clip); else - RemoveFlags(NodeFlags.Clip); + RemoveNodeFlags(NodeFlags.Clip); } } diff --git a/AetherBags/Plugin.cs b/AetherBags/Plugin.cs index b10ce2e..1da387c 100644 --- a/AetherBags/Plugin.cs +++ b/AetherBags/Plugin.cs @@ -1,5 +1,4 @@ using System.Numerics; -using AetherBags.AddonLifecycles; using AetherBags.Addons; using AetherBags.Commands; using AetherBags.Helpers; @@ -7,6 +6,7 @@ using AetherBags.Hooks; using AetherBags.Inventory; using AetherBags.Inventory.Context; using AetherBags.IPC; +using AetherBags.Monitoring; using Dalamud.Plugin; using KamiToolKit; @@ -16,7 +16,7 @@ public class Plugin : IDalamudPlugin { private readonly CommandHandler _commandHandler; private readonly InventoryHooks _inventoryHooks; - private readonly InventoryLifecycles _inventoryLifecycles; + private readonly InventoryMonitor inventoryMonitor; public Plugin(IDalamudPluginInterface pluginInterface) { @@ -72,14 +72,14 @@ public class Plugin : IDalamudPlugin } _inventoryHooks = new InventoryHooks(); - _inventoryLifecycles = new InventoryLifecycles(); + inventoryMonitor = new InventoryMonitor(); } public void Dispose() { InventoryAddonContextMenu.Close(); _inventoryHooks.Dispose(); - _inventoryLifecycles.Dispose(); + inventoryMonitor.Dispose(); System.LootedItemsTracker.Dispose(); System.IPC.Dispose(); @@ -99,10 +99,8 @@ public class Plugin : IDalamudPlugin System.Config = Util.LoadConfigOrDefault(); System.LootedItemsTracker.Enable(); -#if DEBUG - System.AddonInventoryWindow.Toggle(); - System.AddonConfigurationWindow.Toggle(); -#endif + System.AddonInventoryWindow.DebugOpen(); + System.AddonConfigurationWindow.DebugOpen(); } private void OnLogout(int type, int code) diff --git a/AetherBags/System.cs b/AetherBags/System.cs index a25528a..d86af5f 100644 --- a/AetherBags/System.cs +++ b/AetherBags/System.cs @@ -2,6 +2,7 @@ using AetherBags.Addons; using AetherBags.Configuration; using AetherBags.Inventory; using AetherBags.IPC; +using AetherBags.Monitoring; namespace AetherBags; diff --git a/KamiToolKit b/KamiToolKit index 5886600..811154c 160000 --- a/KamiToolKit +++ b/KamiToolKit @@ -1 +1 @@ -Subproject commit 5886600a043640588a7e89eba35d6385b923db6c +Subproject commit 811154c8f882d1dfbb4fa3457982ef802fbff35a