diff --git a/AetherBags/Addons/AddonInventoryWindow.cs b/AetherBags/Addons/AddonInventoryWindow.cs index ebb0d5b..5699ee3 100644 --- a/AetherBags/Addons/AddonInventoryWindow.cs +++ b/AetherBags/Addons/AddonInventoryWindow.cs @@ -42,11 +42,12 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase }; _notificationNode.AttachNode(this); - SearchInputNode = new TextInputWithHintNode + SearchInputNode = new TextInputWithButtonNode { Position = header.SearchPosition, Size = header.SearchSize, - OnInputReceived = _ => RefreshCategoriesCore(autosize: false), + OnInputReceived = _ => ItemRefresh(), + OnButtonClicked = () => InventoryAddonContextMenu.OpenMain(this) }; SearchInputNode.AttachNode(this); diff --git a/AetherBags/Addons/AddonRetainerWindow.cs b/AetherBags/Addons/AddonRetainerWindow.cs index 7f0615b..f710013 100644 --- a/AetherBags/Addons/AddonRetainerWindow.cs +++ b/AetherBags/Addons/AddonRetainerWindow.cs @@ -51,11 +51,12 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase var header = CalculateHeaderLayout(addon); - SearchInputNode = new TextInputWithHintNode + SearchInputNode = new TextInputWithButtonNode { Position = header.SearchPosition, Size = header.SearchSize, - OnInputReceived = _ => RefreshCategoriesCore(autosize: false), + OnInputReceived = _ => ItemRefresh(), + OnButtonClicked = () => InventoryAddonContextMenu.OpenMain(this) }; SearchInputNode.AttachNode(this); diff --git a/AetherBags/Addons/AddonSaddleBagWindow.cs b/AetherBags/Addons/AddonSaddleBagWindow.cs index 336be21..71ceabc 100644 --- a/AetherBags/Addons/AddonSaddleBagWindow.cs +++ b/AetherBags/Addons/AddonSaddleBagWindow.cs @@ -44,11 +44,12 @@ public unsafe class AddonSaddleBagWindow : InventoryAddonBase var header = CalculateHeaderLayout(addon); - SearchInputNode = new TextInputWithHintNode + SearchInputNode = new TextInputWithButtonNode { Position = header.SearchPosition, Size = header.SearchSize, - OnInputReceived = _ => RefreshCategoriesCore(autosize: false), + OnInputReceived = _ => ItemRefresh(), + OnButtonClicked = () => InventoryAddonContextMenu.OpenMain(this) }; SearchInputNode.AttachNode(this); diff --git a/AetherBags/Addons/IInventoryWindow.cs b/AetherBags/Addons/IInventoryWindow.cs index 1f76fbf..b8abd4b 100644 --- a/AetherBags/Addons/IInventoryWindow.cs +++ b/AetherBags/Addons/IInventoryWindow.cs @@ -6,5 +6,6 @@ public interface IInventoryWindow void Toggle(); void Close(); void ManualRefresh(); + void ItemRefresh(); void SetSearchText(string searchText); } \ No newline at end of file diff --git a/AetherBags/Addons/InventoryAddonBase.cs b/AetherBags/Addons/InventoryAddonBase.cs index c9c12e0..843e176 100644 --- a/AetherBags/Addons/InventoryAddonBase.cs +++ b/AetherBags/Addons/InventoryAddonBase.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Numerics; +using AetherBags.Configuration; using AetherBags.Helpers; using AetherBags.Inventory; using AetherBags.Inventory.Categories; @@ -14,6 +15,7 @@ using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit; using KamiToolKit.Classes; +using KamiToolKit.Classes.ContextMenu; using KamiToolKit.Nodes; namespace AetherBags.Addons; @@ -26,11 +28,13 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow protected DragDropNode BackgroundDropTarget = null!; protected WrappingGridNode CategoriesNode = null!; - protected TextInputWithHintNode SearchInputNode = null!; + protected TextInputWithButtonNode SearchInputNode = null!; protected InventoryFooterNode FooterNode = null!; protected TextNode? SlotCounterNode { get; set; } protected CircleButtonNode SettingsButtonNode = null!; + internal ContextMenu ContextMenu = null!; + protected virtual float MinWindowWidth => 600; protected virtual float MaxWindowWidth => 800; protected virtual float MinWindowHeight => 200; @@ -54,6 +58,8 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow protected virtual bool HasPinning => true; protected virtual bool HasSlotCounter => false; + private readonly HashSet _searchMatchScratch = new(); + public void ManualRefresh() { if (!IsOpen) return; @@ -73,6 +79,9 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow } } + + public string GetSearchText() => SearchInputNode?.SearchString.ExtractText() ?? string.Empty; + public virtual void SetSearchText(string searchText) { Services.Framework.RunOnTick(() => @@ -105,14 +114,52 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow if (!_isSetupComplete) return; + var config = System.Config.General; + string searchText = SearchInputNode.SearchString.ExtractText(); + bool isSearching = !string.IsNullOrWhiteSpace(searchText); + + if (config.SearchMode == SearchMode.Highlight && isSearching) + { + _searchMatchScratch.Clear(); + var allData = InventoryState.GetCategories(string.Empty); + + for (int i = 0; i < allData.Count; i++) + { + var cat = allData[i]; + for (int j = 0; j < cat.Items.Count; j++) + { + var item = cat.Items[j]; + if (item.IsRegexMatch(searchText)) + { + _searchMatchScratch.Add(item.Item.ItemId); + } + } + } + HighlightState.SetFilter(HighlightSource.Search, _searchMatchScratch); + } + else + { + HighlightState.ClearFilter(HighlightSource.Search); + } + + if (SearchInputNode != null) + { + bool atActive = !string.IsNullOrEmpty(HighlightState.SelectedAllaganToolsFilterKey); + bool filterModeActive = System.Config.General.SearchMode == SearchMode.Filter; + + SearchInputNode.HintAddColor = (atActive || filterModeActive) + ? new Vector3(0.0f, 0.3f, 0.3f) + : Vector3.Zero; + } + if (HasFooter) { FooterNode.SlotAmountText = InventoryState.GetEmptySlotsString(); FooterNode.RefreshCurrencies(); } - string filter = SearchInputNode.SearchString.ExtractText(); - var categories = InventoryState.GetCategories(filter); + string dataFilter = config.SearchMode == SearchMode.Filter ? searchText : string.Empty; + var categories = InventoryState.GetCategories(dataFilter); float maxContentWidth = MaxWindowWidth - (ContentStartPosition.X * 2); int maxItemsPerLine = CalculateOptimalItemsPerLine(maxContentWidth); @@ -125,6 +172,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow { node.CategorizedInventory = data; node.ItemsPerLine = Math.Min(data.Items.Count, maxItemsPerLine); + node.RefreshNodeVisuals(); }, createNodeMethod: _ => CreateCategoryNode()); @@ -155,24 +203,27 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow protected HeaderLayout CalculateHeaderLayout(AtkUnitBase* addon) { - var size = new Vector2(addon->Size.X / 2.0f, 28.0f); var header = addon->WindowHeaderCollisionNode; - float headerW = header->Width; float headerH = header->Height; - float x = header->X + (headerW - size.X) * 0.5f; - float y = header->Y + (headerH - size.Y) * 0.5f; + // Center the search bar, width is 50% of header + float searchWidth = headerW * 0.5f; + var searchSize = new Vector2(searchWidth, 28f); + + float searchX = (headerW - searchWidth) * 0.5f; + float itemY = header->Y + (headerH - 28f) * 0.5f; return new HeaderLayout { - SearchPosition = new Vector2(x, y), - SearchSize = size, + SearchPosition = new Vector2(searchX, itemY), + SearchSize = searchSize, HeaderWidth = headerW, - HeaderY = y, + HeaderY = itemY }; } + protected void InitializeBackgroundDropTarget() { BackgroundDropTarget = new DragDropNode @@ -344,6 +395,8 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow protected void ResizeWindow(float width, float height) => ResizeWindow(width, height, recalcLayout: true); + public void ItemRefresh() => RefreshCategoriesCore(false); + protected override void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { base.OnRequestedUpdate(addon, numberArrayData, stringArrayData); @@ -352,6 +405,13 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow RefreshCategoriesCore(autosize: true); } + protected override void OnSetup(AtkUnitBase* addon) + { + ContextMenu = new ContextMenu(); + + base.OnSetup(addon); + } + protected override void OnUpdate(AtkUnitBase* addon) { if (RefreshQueued) @@ -368,6 +428,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow protected override void OnFinalize(AtkUnitBase* addon) { + ContextMenu?.Dispose(); HoverSubscribed.Clear(); RefreshQueued = false; RefreshAutosizeQueued = false; diff --git a/AetherBags/Addons/InventoryAddonContextMenu.cs b/AetherBags/Addons/InventoryAddonContextMenu.cs new file mode 100644 index 0000000..60bb32d --- /dev/null +++ b/AetherBags/Addons/InventoryAddonContextMenu.cs @@ -0,0 +1,74 @@ +using System; +using AetherBags. Configuration; +using AetherBags. Inventory; +using AetherBags.Inventory.Context; +using KamiToolKit.Classes. ContextMenu; + +namespace AetherBags.Addons; + +public static class InventoryAddonContextMenu +{ + private static ContextMenuItem Separator => new() + { + Name = "---------------------------", + IsEnabled = false, + OnClick = () => { } + }; + + public static void OpenMain(InventoryAddonBase parent) + { + if (parent?.ContextMenu == null || System.Config == null) return; + + var menu = parent.ContextMenu; + menu.Clear(); + + bool hasActiveAtFilter = !string.IsNullOrEmpty(HighlightState.SelectedAllaganToolsFilterKey); + string searchText = parent.GetSearchText(); + if (HighlightState. IsFilterActive || hasActiveAtFilter || ! string.IsNullOrEmpty(searchText)) + { + menu.AddItem("Clear All Filters", () => + { + HighlightState.ClearAll(); + parent. SetSearchText(string.Empty); + InventoryOrchestrator.RefreshAll(updateMaps: false); + }); + menu.AddItem(Separator); + } + + var currentMode = System. Config.General.SearchMode; + string modeLabel = currentMode == SearchMode. Filter ? "Mode: Hide Non-Matches" : "Mode: Fade Non-Matches"; + menu.AddItem(modeLabel, () => + { + System.Config.General.SearchMode = currentMode == SearchMode. Filter ? SearchMode. Highlight : SearchMode.Filter; + parent.ManualRefresh(); + }); + + if (System.IPC.AllaganTools is { IsReady: true } && System. Config.Categories.AllaganToolsCategoriesEnabled) + { + var atFilters = System.IPC.AllaganTools.GetSearchFilters(); + if (atFilters is { Count: > 0 }) + { + var subMenu = new ContextMenuSubItem + { + Name = "Allagan Tools Filters...", + OnClick = () => { } + }; + + foreach (var (key, name) in atFilters) + { + var capturedKey = key; + bool isActive = HighlightState.SelectedAllaganToolsFilterKey == key; + subMenu.AddItem(isActive ? $"✓ {name}" : $" {name}", () => + { + HighlightState.SelectedAllaganToolsFilterKey = isActive ? string.Empty : capturedKey; + InventoryOrchestrator.RefreshAll(updateMaps: false); + }); + } + + menu.AddItem(subMenu); + } + } + + menu.Open(); + } +} \ No newline at end of file diff --git a/AetherBags/Configuration/CategorySettings.cs b/AetherBags/Configuration/CategorySettings.cs index 1a3d336..12547d3 100644 --- a/AetherBags/Configuration/CategorySettings.cs +++ b/AetherBags/Configuration/CategorySettings.cs @@ -11,7 +11,9 @@ public class CategorySettings public bool CategoriesEnabled { get; set; } = true; public bool GameCategoriesEnabled { get; set; } = true; public bool UserCategoriesEnabled { get; set; } = true; + public bool BisBuddyEnabled { get; set; } = true; public bool AllaganToolsCategoriesEnabled { get; set; } = false; + public AllaganToolsFilterMode AllaganToolsMode { get; set; } = AllaganToolsFilterMode.Highlight; public List UserCategories { get; set; } = new(); } @@ -77,4 +79,10 @@ public enum ToggleFilterState Ignored = 0, Allow = 1, Disallow = 2, +} + +public enum AllaganToolsFilterMode +{ + Categorize = 0, + Highlight = 1, } \ No newline at end of file diff --git a/AetherBags/Configuration/GeneralSettings.cs b/AetherBags/Configuration/GeneralSettings.cs index 4845fbc..79583f5 100644 --- a/AetherBags/Configuration/GeneralSettings.cs +++ b/AetherBags/Configuration/GeneralSettings.cs @@ -3,6 +3,7 @@ namespace AetherBags.Configuration; public class GeneralSettings { public InventoryStackMode StackMode { get; set; } = InventoryStackMode.AggregateByItemId; + public SearchMode SearchMode { get; set; } = SearchMode.Highlight; public bool DebugEnabled { get; set; } = false; public bool CompactPackingEnabled { get; set; } = true; public int CompactLookahead { get; set; } = 24; @@ -22,4 +23,10 @@ public enum InventoryStackMode : byte { NaturalStacks = 0, AggregateByItemId = 1, +} + +public enum SearchMode : byte +{ + Filter = 0, + Highlight = 1, } \ No newline at end of file diff --git a/AetherBags/Extensions/LoggerExtensions.cs b/AetherBags/Extensions/LoggerExtensions.cs index 772ff9b..3882e87 100644 --- a/AetherBags/Extensions/LoggerExtensions.cs +++ b/AetherBags/Extensions/LoggerExtensions.cs @@ -2,13 +2,16 @@ namespace AetherBags.Extensions; public static class LoggerExtensions { - public static void DebugOnly(this object logger, string message) + extension(object logger) { - if(System.Config.General.DebugEnabled) Services.Logger.DebugOnly(message); - } + public void DebugOnly(string message) + { + if (System.Config?.General?.DebugEnabled == true) + { + Services.Logger.DebugOnly(message); + } + } - public static void DebugOnly(this object logger, string message, params object[] args) - { - if(System.Config.General.DebugEnabled) Services.Logger.DebugOnly(message); + public void DebugOnly(string message, params object[] args) => DebugOnly(logger, string.Format(message, args)); } } \ No newline at end of file diff --git a/AetherBags/IPC/AllaganToolsIPC.cs b/AetherBags/IPC/AllaganToolsIPC.cs index 9a9407a..c534b99 100644 --- a/AetherBags/IPC/AllaganToolsIPC.cs +++ b/AetherBags/IPC/AllaganToolsIPC.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using AetherBags.Inventory; +using AetherBags.Inventory.Context; using Dalamud.Plugin.Ipc; namespace AetherBags.IPC; @@ -46,7 +48,6 @@ public class AllaganToolsIPC : IDisposable _initialized.Subscribe(OnAllaganInitialized); - // Check if already initialized try { IsReady = _isInitialized.InvokeFunc(); @@ -181,6 +182,12 @@ public class AllaganToolsIPC : IDisposable } } + public void SelectFilter(string filterKey) + { + HighlightState.SelectedAllaganToolsFilterKey = filterKey; + InventoryOrchestrator.RefreshHighlights(); + } + public void Dispose() { _initialized?.Unsubscribe(OnAllaganInitialized); diff --git a/AetherBags/IPC/BisBuddyIPC.cs b/AetherBags/IPC/BisBuddyIPC.cs new file mode 100644 index 0000000..1317fe4 --- /dev/null +++ b/AetherBags/IPC/BisBuddyIPC.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using AetherBags.Inventory; +using AetherBags.Inventory.Context; +using Dalamud.Plugin.Ipc; + +namespace AetherBags.IPC; + +public class BisBuddyIPC : IDisposable +{ + private ICallGateSubscriber? _isInitialized; + private ICallGateSubscriber? _initialized; + private ICallGateSubscriber>? _getBisItems; + private ICallGateSubscriber, bool>? _bisItemsChanged; + + public bool IsReady { get; private set; } + private static readonly Vector3 BisColor = new(0.0f, 0.3f, 0.0f); + + public BisBuddyIPC() + { + try + { + _isInitialized = Services.PluginInterface.GetIpcSubscriber("BisBuddy.IsInitialized"); + _initialized = Services.PluginInterface.GetIpcSubscriber("BisBuddy.Initialized"); + _getBisItems = Services.PluginInterface.GetIpcSubscriber>("BisBuddy.GetBisItems"); + _bisItemsChanged = Services.PluginInterface.GetIpcSubscriber, bool>("BisBuddy.BisItemsChanged"); + + _initialized.Subscribe(OnInitialized); + _bisItemsChanged.Subscribe(UpdateHighlights); + + try { IsReady = _isInitialized.InvokeFunc(); } catch { IsReady = false; } + + if (IsReady) RequestUpdate(); + } + catch (Exception ex) + { + Services.Logger.DebugOnly($"BisBuddy not available: {ex.Message}"); + } + } + + private void OnInitialized(bool ready) + { + IsReady = ready; + if (ready) RequestUpdate(); + else HighlightState.ClearLabel(HighlightSource.BiSBuddy); + } + + public void RequestUpdate() + { + if (!IsReady) return; + try + { + var items = _getBisItems?.InvokeFunc(); + if (items != null) UpdateHighlights(items); + } + catch { IsReady = false; } + } + + private void UpdateHighlights(List? itemIds) + { + if (!System.Config.Categories.BisBuddyEnabled || itemIds == null || itemIds.Count == 0) + { + HighlightState.ClearLabel(HighlightSource.BiSBuddy); + } + else + { + HighlightState.SetLabel(HighlightSource.BiSBuddy, itemIds, BisColor); + } + + InventoryOrchestrator.RefreshHighlights(); + } + + public void Dispose() + { + _initialized?.Unsubscribe(OnInitialized); + _bisItemsChanged?.Unsubscribe(UpdateHighlights); + } +} \ No newline at end of file diff --git a/AetherBags/IPC/IPCService.cs b/AetherBags/IPC/IPCService.cs index 955e53f..73d2a5e 100644 --- a/AetherBags/IPC/IPCService.cs +++ b/AetherBags/IPC/IPCService.cs @@ -6,18 +6,13 @@ namespace AetherBags.IPC; public class IPCService : IDisposable { - public AllaganToolsIPC AllaganTools { get; } - public WotsItIPC WotsIt { get; } - // Future: public BiSBuddyIPC BiSBuddy { get; } - - public IPCService() - { - AllaganTools = new AllaganToolsIPC(); - WotsIt = new WotsItIPC(); - } + public AllaganToolsIPC AllaganTools { get; } = new(); + public WotsItIPC WotsIt { get; } = new(); + public BisBuddyIPC BisBuddy { get; } = new(); public void Dispose() { AllaganTools.Dispose(); + WotsIt.Dispose(); } } \ No newline at end of file diff --git a/AetherBags/Inventory/Categories/CategoryBucketManager.cs b/AetherBags/Inventory/Categories/CategoryBucketManager.cs index 774b17b..0ca3e4d 100644 --- a/AetherBags/Inventory/Categories/CategoryBucketManager.cs +++ b/AetherBags/Inventory/Categories/CategoryBucketManager.cs @@ -165,7 +165,7 @@ public static class CategoryBucketManager bool allaganCategoriesEnabled) { if (!allaganCategoriesEnabled) return; - if (! System.IPC.AllaganTools.IsReady) return; + if (!System.IPC.AllaganTools.IsReady) return; var filters = System.IPC.AllaganTools.CachedSearchFilters; var filterItems = System.IPC.AllaganTools.CachedFilterItems; diff --git a/AetherBags/Inventory/Context/HighlightState.cs b/AetherBags/Inventory/Context/HighlightState.cs new file mode 100644 index 0000000..e278e5f --- /dev/null +++ b/AetherBags/Inventory/Context/HighlightState.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Numerics; + +namespace AetherBags.Inventory.Context; + +public enum HighlightSource +{ + Search, + AllaganTools, + BiSBuddy, +} + +public static class HighlightState +{ + private static readonly Dictionary> Filters = new(); + private static readonly Dictionary ids, Vector3 color)> Labels = new(); + + public static string? SelectedAllaganToolsFilterKey { get; set; } = string.Empty; + + public static bool IsFilterActive => Filters.Count > 0; + + public static void SetFilter(HighlightSource source, IEnumerable ids) + => Filters[source] = new HashSet(ids); + + public static bool IsInActiveFilters(uint itemId) + { + if (Filters.Count == 0) return true; + foreach (var filter in Filters.Values) + if (filter.Contains(itemId)) return true; + return false; + } + + public static Vector3? GetLabelColor(uint itemId) + { + foreach (var label in Labels.Values) + if (label.ids.Contains(itemId)) return label.color; + return null; + } + + public static void SetLabel(HighlightSource source, IEnumerable ids, Vector3 color) + => Labels[source] = (new HashSet(ids), color); + + + public static void ClearAll() + { + Filters.Clear(); + Labels.Clear(); + SelectedAllaganToolsFilterKey = string.Empty; + } + + public static void ClearFilter(HighlightSource source) => Filters.Remove(source); + public static void ClearLabel(HighlightSource source) => Labels.Remove(source); +} \ No newline at end of file diff --git a/AetherBags/Inventory/InventoryOrchestrator.cs b/AetherBags/Inventory/InventoryOrchestrator.cs index a2f9b0b..36c3632 100644 --- a/AetherBags/Inventory/InventoryOrchestrator.cs +++ b/AetherBags/Inventory/InventoryOrchestrator.cs @@ -42,6 +42,17 @@ public static unsafe class InventoryOrchestrator } } + public static void RefreshHighlights() + { + Services.Framework.RunOnTick(() => + { + foreach (var window in GetAllWindows()) + { + window.ItemRefresh(); + } + }); + } + private static IEnumerable GetAllWindows() { yield return System.AddonInventoryWindow; diff --git a/AetherBags/Inventory/Items/ItemInfo.cs b/AetherBags/Inventory/Items/ItemInfo.cs index 8a0ffe6..b4a59fd 100644 --- a/AetherBags/Inventory/Items/ItemInfo.cs +++ b/AetherBags/Inventory/Items/ItemInfo.cs @@ -82,38 +82,55 @@ public sealed class ItemInfo : IEquatable { get { - uint contextId = InventoryContextState.ActiveContextId; - if (contextId == 0) return true; - - bool isRetainerContext = contextId == 4; - bool isSaddlebagContext = contextId == 29; - bool isMainContext = !isRetainerContext && isSaddlebagContext == false; - - if (IsMainInventory) - { - if (!isMainContext) return true; - - return InventoryContextState.IsEligible(InventoryPage, Item.Slot); - } - - if (Item.Container.IsRetainer) - { - // ...but the context isn't for Retainers, don't dim it. - if (!isRetainerContext) - return true; - } - - // 3. If we are looking at a Saddlebag item... - if (Item.Container.IsSaddleBag) - { - if (!isSaddlebagContext) - return true; - } + if (IsSlotBlocked) return false; + if (!CheckNativeContextEligibility()) return false; + if (!HighlightState.IsInActiveFilters(Item.ItemId)) return false; return true; } } + public float VisualAlpha => IsEligibleForContext ? 1.0f : 0.4f; + + public Vector3 HighlightOverlayColor + { + get + { + if (!System.Config.Categories.BisBuddyEnabled) + return Vector3.Zero; + + return HighlightState.GetLabelColor(Item.ItemId) ?? Vector3.Zero; + } + } + + private bool CheckNativeContextEligibility() + { + uint contextId = InventoryContextState.ActiveContextId; + if (contextId == 0) return true; + + bool isRetainerContext = contextId == 4; + bool isSaddlebagContext = contextId == 29; + bool isMainContext = !isRetainerContext && isSaddlebagContext == false; + + if (IsMainInventory) + { + if (!isMainContext) return true; + return InventoryContextState.IsEligible(InventoryPage, Item.Slot); + } + + if (Item.Container.IsRetainer) + { + if (!isRetainerContext) return true; + } + + if (Item.Container.IsSaddleBag) + { + if (!isSaddlebagContext) return true; + } + + return true; + } + public bool IsMainInventory => InventoryPage >= 0; public bool IsRegexMatch(string searchTerms) diff --git a/AetherBags/Inventory/State/InventoryStateBase.cs b/AetherBags/Inventory/State/InventoryStateBase.cs index 7c57fe8..1d8a1af 100644 --- a/AetherBags/Inventory/State/InventoryStateBase.cs +++ b/AetherBags/Inventory/State/InventoryStateBase.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using AetherBags.Configuration; using AetherBags.Inventory.Categories; +using AetherBags.Inventory.Context; using AetherBags.Inventory.Items; using AetherBags.Inventory.Scanning; using FFXIVClientStructs.FFXIV.Client.Game; @@ -76,8 +77,19 @@ public abstract class InventoryStateBase if (allaganCategoriesEnabled) { - CategoryBucketManager.BucketByAllaganFilters( - ItemInfoByKey, BucketsByKey, ClaimedKeys, allaganCategoriesEnabled); + if (config.Categories.AllaganToolsMode == AllaganToolsFilterMode.Categorize) + { + CategoryBucketManager.BucketByAllaganFilters(ItemInfoByKey, BucketsByKey, ClaimedKeys, true); + HighlightState.ClearFilter(HighlightSource.AllaganTools); + } + else + { + UpdateAllaganHighlight(HighlightState.SelectedAllaganToolsFilterKey); + } + } + else + { + HighlightState.ClearFilter(HighlightSource.AllaganTools); } if (gameCategoriesEnabled) @@ -92,6 +104,25 @@ public abstract class InventoryStateBase } } + private void UpdateAllaganHighlight(string? filterKey) + { + if (string.IsNullOrEmpty(filterKey) || !System.IPC.AllaganTools.IsReady) + { + HighlightState.ClearFilter(HighlightSource.AllaganTools); + return; + } + + var filterItems = System.IPC.AllaganTools.GetFilterItems(filterKey); + if (filterItems != null) + { + HighlightState.SetFilter(HighlightSource.AllaganTools, filterItems.Keys); + } + else + { + HighlightState.ClearFilter(HighlightSource.AllaganTools); + } + } + public IReadOnlyList GetCategories(string filter = "", bool invert = false) => InventoryFilter.FilterCategories(AllCategories, BucketsByKey, FilteredCategories, filter, invert); diff --git a/AetherBags/Nodes/Configuration/Category/CategoryConfigurationNode.cs b/AetherBags/Nodes/Configuration/Category/CategoryConfigurationNode.cs index 127d235..ca81a03 100644 --- a/AetherBags/Nodes/Configuration/Category/CategoryConfigurationNode.cs +++ b/AetherBags/Nodes/Configuration/Category/CategoryConfigurationNode.cs @@ -16,7 +16,7 @@ public class CategoryConfigurationNode : ConfigNode { _categoryList = new ScrollingListNode { - AutoHideScrollbar = true, + AutoHideScrollBar = true, }; _categoryList.FitContents = true; _categoryList.AttachNode(this); diff --git a/AetherBags/Nodes/Configuration/Category/CategoryGeneralConfigurationNode.cs b/AetherBags/Nodes/Configuration/Category/CategoryGeneralConfigurationNode.cs index c7ec4af..01e69c1 100644 --- a/AetherBags/Nodes/Configuration/Category/CategoryGeneralConfigurationNode.cs +++ b/AetherBags/Nodes/Configuration/Category/CategoryGeneralConfigurationNode.cs @@ -1,7 +1,12 @@ +using System; +using System.Linq; using System.Numerics; using AetherBags.Configuration; using AetherBags.Inventory; +using AetherBags.Inventory.Context; using AetherBags.Nodes.Color; +using AetherBags.Nodes.Input; +using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Classes; using KamiToolKit.Nodes; @@ -71,7 +76,47 @@ public sealed class CategoryGeneralConfigurationNode : TabbedVerticalListNode }; AddNode(userCategoriesEnabled); + bool bisBuddyReady = System.IPC.BisBuddy?.IsReady ?? false; + + CheckboxNode bisBuddyEnabled = new CheckboxNode + { + Size = Size with { Y = 18 }, + IsVisible = true, + String = bisBuddyReady ? "BISBuddy" : "BISBuddy (Not Available)", + IsChecked = config.BisBuddyEnabled, + TextTooltip = "Allow BISBuddy to highlight items.", + OnClick = isChecked => + { + config.BisBuddyEnabled = isChecked; + System.IPC.BisBuddy?.RequestUpdate(); + RefreshInventory(); + } + }; + AddNode(bisBuddyEnabled); + bool allaganReady = System.IPC.AllaganTools?.IsReady ?? false; + + LabeledDropdownNode? atModeDropdown = new LabeledDropdownNode + { + Size = new Vector2(300, 20), + LabelText = "Filter Display Mode", + LabelTextFlags = TextFlags.AutoAdjustNodeSize, + IsEnabled = config.AllaganToolsCategoriesEnabled && allaganReady, + Options = Enum.GetNames(typeof(AllaganToolsFilterMode)).ToList(), + SelectedOption = config.AllaganToolsMode.ToString(), + OnOptionSelected = selected => + { + if (Enum.TryParse(selected, out var parsed)) + { + config.AllaganToolsMode = parsed; + if (parsed == AllaganToolsFilterMode.Categorize) + HighlightState.ClearFilter(HighlightSource.AllaganTools); + + RefreshInventory(); + } + } + }; + _allaganToolsCheckbox = new CheckboxNode { Size = Size with { Y = 18 }, @@ -85,14 +130,16 @@ public sealed class CategoryGeneralConfigurationNode : TabbedVerticalListNode OnClick = isChecked => { config.AllaganToolsCategoriesEnabled = isChecked; - if (isChecked) - { - System.IPC?.AllaganTools?.RefreshFilters(); - } + if (atModeDropdown != null) atModeDropdown.IsEnabled = isChecked; + if (isChecked) System.IPC?.AllaganTools?.RefreshFilters(); RefreshInventory(); } }; AddNode(_allaganToolsCheckbox); + + AddTab(1); + AddNode(atModeDropdown); + SubtractTab(1); } private void RefreshInventory() => InventoryOrchestrator.RefreshAll(updateMaps: true); diff --git a/AetherBags/Nodes/Configuration/General/FunctionalConfigurationNode.cs b/AetherBags/Nodes/Configuration/General/FunctionalConfigurationNode.cs index d3b8d5d..e0a6342 100644 --- a/AetherBags/Nodes/Configuration/General/FunctionalConfigurationNode.cs +++ b/AetherBags/Nodes/Configuration/General/FunctionalConfigurationNode.cs @@ -132,6 +132,24 @@ internal sealed class FunctionalConfigurationNode : TabbedVerticalListNode }; AddNode(linkItemCheckBox); + var searchModeDropDown = new LabeledDropdownNode + { + Size = new Vector2(300, 20), + LabelText = "Search Mode", + LabelTextFlags = TextFlags.AutoAdjustNodeSize, + Options = Enum.GetNames(typeof(SearchMode)).ToList(), + SelectedOption = config.SearchMode.ToString(), + OnOptionSelected = selected => + { + if (Enum.TryParse(selected, out var parsed)) + { + config.SearchMode = parsed; + InventoryOrchestrator.RefreshAll(updateMaps: false); + } + } + }; + AddNode(searchModeDropDown); + _stackDropDown = new LabeledDropdownNode { Size = new Vector2(300, 20), diff --git a/AetherBags/Nodes/Configuration/General/GeneralScrollingAreaNode.cs b/AetherBags/Nodes/Configuration/General/GeneralScrollingAreaNode.cs index 75378a1..73c04ed 100644 --- a/AetherBags/Nodes/Configuration/General/GeneralScrollingAreaNode.cs +++ b/AetherBags/Nodes/Configuration/General/GeneralScrollingAreaNode.cs @@ -7,8 +7,6 @@ namespace AetherBags.Nodes.Configuration.General; public sealed class GeneralScrollingAreaNode : ScrollingListNode { - private readonly CheckboxNode _debugCheckboxNode = null!; - public GeneralScrollingAreaNode() { GeneralSettings config = System.Config.General; diff --git a/AetherBags/Nodes/Input/TextInputWithButtonNode.cs b/AetherBags/Nodes/Input/TextInputWithButtonNode.cs new file mode 100644 index 0000000..8b9d56f --- /dev/null +++ b/AetherBags/Nodes/Input/TextInputWithButtonNode.cs @@ -0,0 +1,55 @@ +using System; +using System.Numerics; +using KamiToolKit.Classes; +using KamiToolKit.Nodes; +using Lumina.Text.ReadOnly; + +namespace AetherBags.Nodes.Input; + +public class TextInputWithButtonNode : SimpleComponentNode { + private readonly TextInputNode _textInputNode; + private readonly CircleButtonNode _contextButton; + + public Action? OnButtonClicked { + get => _contextButton.OnClick; + set => _contextButton.OnClick = value; + } + + public TextInputWithButtonNode() { + _textInputNode = new TextInputNode { + PlaceholderString = "Search . . .", + }; + _textInputNode.AttachNode(this); + + _contextButton = new CircleButtonNode { + Icon = ButtonIcon.Filter, + Size = new Vector2(28f), + }; + _contextButton.AttachNode(this); + } + + public Vector3 HintAddColor { + get => _contextButton.AddColor; + set => _contextButton.AddColor = value; + } + + public required Action? OnInputReceived { + get => _textInputNode.OnInputReceived; + set => _textInputNode.OnInputReceived = value; + } + + protected override void OnSizeChanged() { + base.OnSizeChanged(); + + _contextButton.Size = new Vector2(Height, Height); + _contextButton.Position = new Vector2(Width - _contextButton.Width, 0.0f); + + _textInputNode.Size = new Vector2(Width - _contextButton.Width - 5.0f, Height); + _textInputNode.Position = new Vector2(0.0f, 0.0f); + } + + public ReadOnlySeString SearchString { + get => _textInputNode.SeString; + set => _textInputNode.SeString = value; + } +} \ No newline at end of file diff --git a/AetherBags/Nodes/Input/TextInputWithHintNode.cs b/AetherBags/Nodes/Input/TextInputWithHintNode.cs deleted file mode 100644 index 8d84ff2..0000000 --- a/AetherBags/Nodes/Input/TextInputWithHintNode.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Numerics; -using KamiToolKit.Nodes; -using Lumina.Text; -using Lumina.Text.ReadOnly; - -namespace AetherBags.Nodes.Input; - -public class TextInputWithHintNode : SimpleComponentNode { - private readonly TextInputNode _textInputNode; - private readonly ImageNode _helpNode; - - public TextInputWithHintNode() { - _textInputNode = new TextInputNode { - PlaceholderString = "Search . . .", - }; - _textInputNode.AttachNode(this); - - _helpNode = new SimpleImageNode { - TexturePath = "ui/uld/CircleButtons.tex", - TextureCoordinates = new Vector2(112.0f, 84.0f), - TextureSize = new Vector2(28.0f, 28.0f), - TextTooltip = new SeStringBuilder() - .Append("Supports Regex Search") - .AppendNewLine() - .Append("Start input with '$' to search by description") - .ToReadOnlySeString(), - }; - _helpNode.AttachNode(this); - } - - public required Action? OnInputReceived { - get => _textInputNode.OnInputReceived; - set => _textInputNode.OnInputReceived = value; - } - - protected override void OnSizeChanged() { - base.OnSizeChanged(); - - _helpNode.Size = new Vector2(Height, Height); - _helpNode.Position = new Vector2(Width - _helpNode.Width - 5.0f, 0.0f); - - _textInputNode.Size = new Vector2(Width - _helpNode.Width - 5.0f, Height); - _textInputNode.Position = new Vector2(0.0f, 0.0f); - } - - public ReadOnlySeString SearchString { - get => _textInputNode.SeString; - set => _textInputNode.SeString = value; - } -} \ No newline at end of file diff --git a/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs index d1b6f51..bf4cef5 100644 --- a/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs +++ b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs @@ -14,8 +14,6 @@ using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Classes; using KamiToolKit.Nodes; -// TODO: Switch back to CS version when Dalamud Updated - namespace AetherBags.Nodes.Inventory; public class InventoryCategoryNode : SimpleComponentNode @@ -237,17 +235,15 @@ public class InventoryCategoryNode : SimpleComponentNode ReferenceIndex = (short)absoluteIndex }; - bool isSlotBlocked = item.Container.IsMainInventory && data.IsSlotBlocked; - float alpha = !isSlotBlocked && data.IsEligibleForContext ? 1.0f : 0.4f; - return new InventoryDragDropNode { Size = new Vector2(42, 46), - Alpha = alpha, + Alpha = data.VisualAlpha, + AddColor = data.HighlightOverlayColor, + IsDraggable = !data.IsSlotBlocked, IsVisible = true, IconId = item.IconId, AcceptedType = DragDropType.Item, - IsDraggable = !data.IsSlotBlocked, Payload = nodePayload, IsClickable = true, OnDiscard = node => OnDiscard(node, data), @@ -269,6 +265,18 @@ public class InventoryCategoryNode : SimpleComponentNode }; } + public void RefreshNodeVisuals() + { + foreach (var node in _itemGridNode.Nodes) + { + if (node is not InventoryDragDropNode itemNode || itemNode.ItemInfo == null) continue; + + itemNode.Alpha = itemNode.ItemInfo.VisualAlpha; + itemNode.AddColor = itemNode.ItemInfo.HighlightOverlayColor; + itemNode.IsDraggable = !itemNode.ItemInfo.IsSlotBlocked; + } + } + private unsafe void OnDiscard(DragDropNode node, ItemInfo item) { uint addonId = RaptureAtkUnitManager.Instance()->GetAddonByNode(node)->Id; @@ -299,7 +307,7 @@ public class InventoryCategoryNode : SimpleComponentNode if (acceptedPayload.IsSameBaseContainer(nodePayload)) { - Services.Logger.Information("[OnPayload] Source and target are in the same base container, skipping move."); + Services.Logger.DebugOnly("[OnPayload] Source and target are in the same base container, skipping move."); node.IconId = targetItemInfo.IconId; node.Payload = nodePayload; return; diff --git a/AetherBags/Plugin.cs b/AetherBags/Plugin.cs index 827637d..7b4e031 100644 --- a/AetherBags/Plugin.cs +++ b/AetherBags/Plugin.cs @@ -24,10 +24,11 @@ public unsafe class Plugin : IDalamudPlugin { pluginInterface.Create(); + System.Config = Util.LoadConfigOrDefault(); + BackupHelper.DoConfigBackup(pluginInterface); KamiToolKitLibrary.Initialize(pluginInterface); - System.Config = Util.LoadConfigOrDefault(); System.IPC = new IPCService(); diff --git a/KamiToolKit b/KamiToolKit index 21b3f2b..e6ff0f7 160000 --- a/KamiToolKit +++ b/KamiToolKit @@ -1 +1 @@ -Subproject commit 21b3f2b44c1778cb4f7d5fe36a37c0a03377bb36 +Subproject commit e6ff0f781b4c1d23f948b73bf22419e1a4240e94