IPCs, Improved AT support, BISBuddy support
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -6,5 +6,6 @@ public interface IInventoryWindow
|
||||
void Toggle();
|
||||
void Close();
|
||||
void ManualRefresh();
|
||||
void ItemRefresh();
|
||||
void SetSearchText(string searchText);
|
||||
}
|
||||
@@ -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<InventoryCategoryNode> 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<uint> _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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<UserCategoryDefinition> UserCategories { get; set; } = new();
|
||||
}
|
||||
@@ -77,4 +79,10 @@ public enum ToggleFilterState
|
||||
Ignored = 0,
|
||||
Allow = 1,
|
||||
Disallow = 2,
|
||||
}
|
||||
|
||||
public enum AllaganToolsFilterMode
|
||||
{
|
||||
Categorize = 0,
|
||||
Highlight = 1,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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<bool>? _isInitialized;
|
||||
private ICallGateSubscriber<bool, bool>? _initialized;
|
||||
private ICallGateSubscriber<List<uint>>? _getBisItems;
|
||||
private ICallGateSubscriber<List<uint>, 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<bool>("BisBuddy.IsInitialized");
|
||||
_initialized = Services.PluginInterface.GetIpcSubscriber<bool, bool>("BisBuddy.Initialized");
|
||||
_getBisItems = Services.PluginInterface.GetIpcSubscriber<List<uint>>("BisBuddy.GetBisItems");
|
||||
_bisItemsChanged = Services.PluginInterface.GetIpcSubscriber<List<uint>, 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<uint>? 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<HighlightSource, HashSet<uint>> Filters = new();
|
||||
private static readonly Dictionary<HighlightSource, (HashSet<uint> 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<uint> ids)
|
||||
=> Filters[source] = new HashSet<uint>(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<uint> ids, Vector3 color)
|
||||
=> Labels[source] = (new HashSet<uint>(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);
|
||||
}
|
||||
@@ -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<IInventoryWindow> GetAllWindows()
|
||||
{
|
||||
yield return System.AddonInventoryWindow;
|
||||
|
||||
@@ -82,38 +82,55 @@ public sealed class ItemInfo : IEquatable<ItemInfo>
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -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<CategorizedInventory> GetCategories(string filter = "", bool invert = false)
|
||||
=> InventoryFilter.FilterCategories(AllCategories, BucketsByKey, FilteredCategories, filter, invert);
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ public class CategoryConfigurationNode : ConfigNode<CategoryWrapper>
|
||||
{
|
||||
_categoryList = new ScrollingListNode
|
||||
{
|
||||
AutoHideScrollbar = true,
|
||||
AutoHideScrollBar = true,
|
||||
};
|
||||
_categoryList.FitContents = true;
|
||||
_categoryList.AttachNode(this);
|
||||
|
||||
@@ -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<AllaganToolsFilterMode>(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);
|
||||
|
||||
@@ -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<SearchMode>(selected, out var parsed))
|
||||
{
|
||||
config.SearchMode = parsed;
|
||||
InventoryOrchestrator.RefreshAll(updateMaps: false);
|
||||
}
|
||||
}
|
||||
};
|
||||
AddNode(searchModeDropDown);
|
||||
|
||||
_stackDropDown = new LabeledDropdownNode
|
||||
{
|
||||
Size = new Vector2(300, 20),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<ReadOnlySeString>? 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;
|
||||
}
|
||||
}
|
||||
@@ -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<ReadOnlySeString>? OnInputReceived {
|
||||
get => _textInputNode.OnInputReceived;
|
||||
set => _textInputNode.OnInputReceived = value;
|
||||
}
|
||||
|
||||
protected override void OnSizeChanged() {
|
||||
base.OnSizeChanged();
|
||||
|
||||
_helpNode.Size = new Vector2(Height, Height);
|
||||
_helpNode.Position = new Vector2(Width - _helpNode.Width - 5.0f, 0.0f);
|
||||
|
||||
_textInputNode.Size = new Vector2(Width - _helpNode.Width - 5.0f, Height);
|
||||
_textInputNode.Position = new Vector2(0.0f, 0.0f);
|
||||
}
|
||||
|
||||
public ReadOnlySeString SearchString {
|
||||
get => _textInputNode.SeString;
|
||||
set => _textInputNode.SeString = value;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -24,10 +24,11 @@ public unsafe class Plugin : IDalamudPlugin
|
||||
{
|
||||
pluginInterface.Create<Services>();
|
||||
|
||||
System.Config = Util.LoadConfigOrDefault();
|
||||
|
||||
BackupHelper.DoConfigBackup(pluginInterface);
|
||||
|
||||
KamiToolKitLibrary.Initialize(pluginInterface);
|
||||
System.Config = Util.LoadConfigOrDefault();
|
||||
|
||||
System.IPC = new IPCService();
|
||||
|
||||
|
||||
+1
-1
Submodule KamiToolKit updated: 21b3f2b44c...e6ff0f781b
Reference in New Issue
Block a user