Improve Category editor

This commit is contained in:
Zeffuro
2026-01-05 03:01:37 +01:00
parent d66142dfd2
commit 3d9d399b26
8 changed files with 855 additions and 633 deletions
@@ -5,68 +5,54 @@ using KamiToolKit.Premade.Nodes;
namespace AetherBags.Nodes.Configuration.Category; namespace AetherBags.Nodes.Configuration.Category;
public class CategoryConfigurationNode : ConfigNode<CategoryWrapper> public class CategoryConfigurationNode : ConfigNode<CategoryWrapper>
{ {
private readonly ScrollingListNode _categoryList;
private CategoryDefinitionConfigurationNode? _activeNode; private CategoryDefinitionConfigurationNode? _activeNode;
public Action? OnCategoryChanged { get; set; } public Action? OnCategoryChanged { get; set; }
public CategoryConfigurationNode() public CategoryConfigurationNode()
{ {
_categoryList = new ScrollingListNode
{
AutoHideScrollBar = true,
};
_categoryList.FitContents = true;
_categoryList.AttachNode(this);
} }
protected override void OptionChanged(CategoryWrapper? option) protected override void OptionChanged(CategoryWrapper? option)
{ {
if (option?.CategoryDefinition is null) if (option?.CategoryDefinition is null)
{ {
_categoryList.IsVisible = false; if (_activeNode is not null)
{
_activeNode.IsVisible = false;
}
return; return;
} }
_categoryList.IsVisible = true;
if (_activeNode is null) if (_activeNode is null)
{ {
_activeNode = new CategoryDefinitionConfigurationNode(option.CategoryDefinition) _activeNode = new CategoryDefinitionConfigurationNode
{ {
Size = _categoryList.Size, OnLayoutChanged = RecalculateLayout,
OnLayoutChanged = UpdateScrollHeight,
OnCategoryPropertyChanged = OnCategoryChanged, OnCategoryPropertyChanged = OnCategoryChanged,
}; };
_categoryList.AddNode(_activeNode); _activeNode.AttachNode(this);
}
else
{
_activeNode.SetCategory(option.CategoryDefinition);
} }
UpdateScrollHeight(); _activeNode.IsVisible = true;
_activeNode.Size = Size;
_activeNode.SetCategory(option.CategoryDefinition);
} }
private void UpdateScrollHeight() private void RecalculateLayout()
{ {
_categoryList.RecalculateLayout(); // Trigger parent layout update if needed
//_categoryList.ContentHeight = _categoryList.Height;
} }
protected override void OnSizeChanged() protected override void OnSizeChanged()
{ {
base.OnSizeChanged(); base.OnSizeChanged();
_categoryList.Size = Size;
_categoryList.Width = Width;
foreach (var node in _categoryList.GetNodes<CategoryDefinitionConfigurationNode>()) if (_activeNode is not null)
{ {
node.Width = Width; _activeNode.Size = Size;
} }
UpdateScrollHeight();
} }
} }
@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using AetherBags.Configuration; using AetherBags.Configuration;
using AetherBags.Inventory; using AetherBags.Inventory;
@@ -15,520 +16,500 @@ using Action = System.Action;
namespace AetherBags.Nodes.Configuration.Category; namespace AetherBags.Nodes.Configuration.Category;
public sealed class CategoryDefinitionConfigurationNode : VerticalListNode public sealed class CategoryDefinitionConfigurationNode : SimpleComponentNode
{ {
private readonly CheckboxNode _enabledCheckbox; private static ExcelSheet<Item>? ItemSheet => Services.DataManager.GetExcelSheet<Item>();
private readonly CheckboxNode _pinnedCheckbox; private static ExcelSheet<ItemUICategory>? UICategorySheet => Services.DataManager.GetExcelSheet<ItemUICategory>();
private readonly TextInputNode _nameInputNode;
private readonly TextInputNode _descriptionInputNode;
private readonly ColorInputRow _colorInputNode;
private readonly NumericInputNode _priorityInputNode;
private readonly NumericInputNode _orderInputNode;
private readonly CheckboxNode _levelEnabledCheckbox; public Action? OnLayoutChanged { get; init; }
private readonly NumericInputNode _levelMinNode; public Action? OnCategoryPropertyChanged { get; init; }
private readonly NumericInputNode _levelMaxNode;
private readonly CheckboxNode _itemLevelEnabledCheckbox; private UserCategoryDefinition _categoryDefinition = new();
private readonly NumericInputNode _itemLevelMinNode;
private readonly NumericInputNode _itemLevelMaxNode;
private readonly CheckboxNode _vendorPriceEnabledCheckbox; private readonly ScrollingAreaNode<TreeListNode> _scrollingArea;
private readonly NumericInputNode _vendorPriceMinNode; private readonly BasicSettingsSection _basicSettings;
private readonly NumericInputNode _vendorPriceMaxNode; private readonly RangeFiltersSection _rangeFilters;
private readonly StateFiltersSection _stateFilters;
private readonly ListFiltersSection _listFilters;
private readonly StateFilterRowNode _untradableFilter; public CategoryDefinitionConfigurationNode()
private readonly StateFilterRowNode _uniqueFilter;
private readonly StateFilterRowNode _collectableFilter;
private readonly StateFilterRowNode _dyeableFilter;
private readonly StateFilterRowNode _repairableFilter;
private readonly StateFilterRowNode _hqFilter;
private readonly StateFilterRowNode _desynthFilter;
private readonly StateFilterRowNode _glamourFilter;
private readonly StateFilterRowNode _spiritbondFilter;
private readonly UintListEditorNode _allowedItemIdsEditor;
private readonly StringListEditorNode _allowedNamePatternsEditor;
private readonly UintListEditorNode _allowedUiCategoriesEditor;
private readonly RarityEditorNode _allowedRaritiesEditor;
private bool _isInitialized;
private static ExcelSheet<Item>? _sItemSheet;
private static ExcelSheet<ItemUICategory>? _sUICategorySheet;
public Action? OnLayoutChanged { get; set; }
public Action? OnCategoryPropertyChanged { get; set; }
private UserCategoryDefinition CategoryDefinition { get; set; }
public CategoryDefinitionConfigurationNode(UserCategoryDefinition categoryDefinition)
{ {
CategoryDefinition = categoryDefinition; _scrollingArea = new ScrollingAreaNode<TreeListNode>
_sItemSheet ??= Services.DataManager.GetExcelSheet<Item>();
_sUICategorySheet ??= Services.DataManager.GetExcelSheet<ItemUICategory>();
FitContents = true;
ItemSpacing = 4.0f;
var catchAllWarningNode = new TextNode
{ {
Size = new Vector2(300, 40), ContentHeight = 100.0f,
TextFlags = TextFlags.MultiLine | TextFlags.AutoAdjustNodeSize, AutoHideScrollBar = true,
SeString = new SeStringBuilder().Append(" Warning: No rules configured\nThis category won't match anything!").ToReadOnlySeString(),
TextColor = ColorHelper.GetColor(17),
LineSpacing = 20,
IsVisible = UserCategoryMatcher.IsCatchAll(CategoryDefinition),
}; };
AddNode(catchAllWarningNode); _scrollingArea.AttachNode(this);
AddNode(CreateSectionHeader("Basic Settings")); _scrollingArea.ContentNode.OnLayoutUpdate = newHeight =>
{
_scrollingArea.ContentHeight = newHeight;
};
_scrollingArea.ContentNode.CategoryVerticalSpacing = 4.0f;
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)
{
String = "List Filters",
IsCollapsed = true,
OnValueChanged = NotifyChanged,
OnListChanged = HandleListChanged,
};
_listFilters.OnToggle = _ => HandleLayoutChange();
treeListNode.AddCategoryNode(_listFilters);
}
protected override void OnSizeChanged()
{
base.OnSizeChanged();
_scrollingArea.Size = Size;
foreach (var categoryNode in _scrollingArea.ContentNode.CategoryNodes)
{
categoryNode.Width = Width - 16.0f;
}
_scrollingArea.ContentNode.RefreshLayout();
}
public void SetCategory(UserCategoryDefinition newCategory)
{
_categoryDefinition = newCategory;
RefreshAllValues();
}
private void RefreshAllValues()
{
_basicSettings.Refresh();
_rangeFilters.Refresh();
_stateFilters.Refresh();
_listFilters.Refresh();
HandleLayoutChange();
}
private void HandleListChanged()
{
NotifyChanged();
HandleLayoutChange();
}
private void HandleLayoutChange()
{
_scrollingArea.ContentNode.RefreshLayout();
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
{
private readonly Func<UserCategoryDefinition> _getCategoryDefinition;
public Action? OnValueChanged { get; init; }
protected UserCategoryDefinition CategoryDefinition => _getCategoryDefinition();
protected ConfigurationSection(Func<UserCategoryDefinition> getCategoryDefinition)
{
_getCategoryDefinition = getCategoryDefinition;
VerticalPadding = 4.0f;
}
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<UserCategoryDefinition> getCategoryDefinition)
: base(getCategoryDefinition)
{
}
private void EnsureInitialized()
{
if (_initialized) return;
_initialized = true;
_enabledCheckbox = new CheckboxNode _enabledCheckbox = new CheckboxNode
{ {
Size = new Vector2(200, 20), Size = new Vector2(Width, 20),
String = "Enabled", String = "Enabled",
IsChecked = CategoryDefinition.Enabled,
OnClick = isChecked => OnClick = isChecked =>
{ {
CategoryDefinition.Enabled = isChecked; CategoryDefinition.Enabled = isChecked;
NotifyChanged(); OnPropertyChanged?.Invoke();
NotifyCategoryPropertyChanged();
}, },
}; };
AddNode(_enabledCheckbox); AddNode(_enabledCheckbox);
_pinnedCheckbox = new CheckboxNode _pinnedCheckbox = new CheckboxNode
{ {
Size = new Vector2(200, 20), Size = new Vector2(Width, 20),
String = "Pinned", String = "Pinned",
IsChecked = CategoryDefinition.Pinned,
OnClick = isChecked => OnClick = isChecked =>
{ {
CategoryDefinition.Pinned = isChecked; CategoryDefinition.Pinned = isChecked;
NotifyChanged(); OnPropertyChanged?.Invoke();
NotifyCategoryPropertyChanged();
}, },
}; };
AddNode(_pinnedCheckbox); AddNode(_pinnedCheckbox);
AddNode(new LabelTextNode AddNode(CreateLabel("Name: "));
{ _nameInput = new TextInputNode
TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(80, 20),
String = "Name:"
});
_nameInputNode = new TextInputNode
{ {
Size = new Vector2(250, 28), Size = new Vector2(250, 28),
String = CategoryDefinition.Name, PlaceholderString = "Category Name",
PlaceholderString = CategoryDefinition.Name.IsNullOrWhitespace() ? "Category Name" : "", OnInputReceived = input =>
OnInputReceived = name =>
{ {
CategoryDefinition.Name = name.ExtractText(); CategoryDefinition.Name = input.ExtractText();
NotifyChanged(); OnPropertyChanged?.Invoke();
NotifyCategoryPropertyChanged();
}, },
}; };
AddNode(_nameInputNode); AddNode(_nameInput);
AddNode(new LabelTextNode AddNode(CreateLabel("Description:"));
{ _descriptionInput = new TextInputNode
TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(80, 20),
String = "Description:"
});
_descriptionInputNode = new TextInputNode
{ {
Size = new Vector2(250, 28), Size = new Vector2(250, 28),
String = CategoryDefinition.Description, PlaceholderString = "Optional description",
PlaceholderString = CategoryDefinition.Description.IsNullOrWhitespace() ? "Optional description" : "", OnInputReceived = input =>
OnInputReceived = desc =>
{ {
CategoryDefinition.Description = desc.ExtractText(); CategoryDefinition.Description = input.ExtractText();
NotifyChanged(); OnValueChanged?.Invoke();
}, },
}; };
AddNode(_descriptionInputNode); AddNode(_descriptionInput);
_colorInputNode = new ColorInputRow _colorInput = new ColorInputRow
{ {
Label = "Color", Label = "Color",
Size = new Vector2(300, 28), Size = new Vector2(300, 28),
CurrentColor = CategoryDefinition.Color, CurrentColor = new UserCategoryDefinition().Color,
DefaultColor = new UserCategoryDefinition().Color, DefaultColor = new UserCategoryDefinition().Color,
OnColorConfirmed = color => OnColorConfirmed = c => { CategoryDefinition.Color = c; OnValueChanged?.Invoke(); },
{ OnColorCanceled = c => { CategoryDefinition.Color = c; OnValueChanged?.Invoke(); },
CategoryDefinition.Color = color; OnColorPreviewed = c => { CategoryDefinition.Color = c; OnValueChanged?.Invoke(); },
NotifyChanged();
},
OnColorCanceled = color =>
{
CategoryDefinition.Color = color;
NotifyChanged();
},
OnColorPreviewed = color =>
{
CategoryDefinition.Color = color;
NotifyChanged();
}
}; };
AddNode(_colorInputNode); AddNode(_colorInput);
AddNode(new LabelTextNode AddNode(CreateLabel("Priority:"));
{ _priorityInput = new NumericInputNode
TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(80, 20),
String = "Priority:"
});
_priorityInputNode = new NumericInputNode
{ {
Size = new Vector2(120, 28), Size = new Vector2(120, 28),
Min = 0, Min = 0,
Max = 1000, Max = 1000,
Step = 1, Step = 1,
Value = CategoryDefinition.Priority,
OnValueUpdate = val => OnValueUpdate = val =>
{ {
CategoryDefinition.Priority = val; CategoryDefinition.Priority = val;
NotifyChanged(); OnValueChanged?.Invoke();
}, },
}; };
AddNode(_priorityInputNode); AddNode(_priorityInput);
AddNode(new LabelTextNode { TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(80, 20), String = "Order:" }); AddNode(CreateLabel("Order: "));
_orderInputNode = new NumericInputNode _orderInput = new NumericInputNode
{ {
Size = new Vector2(120, 28), Size = new Vector2(120, 28),
Min = 0, Min = 0,
Max = 9999, Max = 9999,
Step = 1, Step = 1,
Value = CategoryDefinition.Order,
OnValueUpdate = val => OnValueUpdate = val =>
{ {
CategoryDefinition.Order = val; CategoryDefinition.Order = val;
NotifyChanged(); OnPropertyChanged?.Invoke();
NotifyCategoryPropertyChanged();
}, },
}; };
AddNode(_orderInputNode); AddNode(_orderInput);
AddNode(CreateSectionHeader("Range Filters")); RecalculateLayout();
}
(_levelEnabledCheckbox, _levelMinNode, _levelMaxNode) = CreateRangeFilter( public void Refresh()
"Level", {
CategoryDefinition.Rules.Level, EnsureInitialized();
0, 200,
(enabled, min, max) => _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<UserCategoryDefinition> 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.Enabled = enabled;
CategoryDefinition.Rules.Level.Min = min; CategoryDefinition.Rules.Level.Min = min;
CategoryDefinition.Rules.Level.Max = max; CategoryDefinition.Rules.Level.Max = max;
NotifyChanged(); OnValueChanged?.Invoke();
} },
); };
AddNode(_levelFilter);
(_itemLevelEnabledCheckbox, _itemLevelMinNode, _itemLevelMaxNode) = CreateRangeFilter( _itemLevelFilter = new RangeFilterRow
"Item Level", {
CategoryDefinition.Rules.ItemLevel, Label = "Item Level",
0, 2000, MinBound = 0,
(enabled, min, max) => MaxBound = 2000,
OnFilterChanged = (enabled, min, max) =>
{ {
CategoryDefinition.Rules.ItemLevel.Enabled = enabled; CategoryDefinition.Rules.ItemLevel.Enabled = enabled;
CategoryDefinition.Rules.ItemLevel.Min = min; CategoryDefinition.Rules.ItemLevel.Min = min;
CategoryDefinition.Rules.ItemLevel.Max = max; CategoryDefinition.Rules.ItemLevel.Max = max;
NotifyChanged(); OnValueChanged?.Invoke();
} },
);
(_vendorPriceEnabledCheckbox, _vendorPriceMinNode, _vendorPriceMaxNode) = CreateRangeFilterUint(
"Vendor Price",
CategoryDefinition.Rules.VendorPrice,
0, 9_999_999
);
AddNode(CreateSectionHeader("State Filters"));
_untradableFilter = new StateFilterRowNode("Untradable", CategoryDefinition.Rules.Untradable, NotifyChanged);
AddNode(_untradableFilter);
_uniqueFilter = new StateFilterRowNode("Unique", CategoryDefinition.Rules.Unique, NotifyChanged);
AddNode(_uniqueFilter);
_collectableFilter = new StateFilterRowNode("Collectable", CategoryDefinition.Rules.Collectable, NotifyChanged);
AddNode(_collectableFilter);
_dyeableFilter = new StateFilterRowNode("Dyeable", CategoryDefinition.Rules.Dyeable, NotifyChanged);
AddNode(_dyeableFilter);
_repairableFilter = new StateFilterRowNode("Repairable", CategoryDefinition.Rules.Repairable, NotifyChanged);
AddNode(_repairableFilter);
_hqFilter = new StateFilterRowNode("High Quality", CategoryDefinition.Rules.HighQuality, NotifyChanged);
AddNode(_hqFilter);
_desynthFilter = new StateFilterRowNode("Desynthesizable", CategoryDefinition.Rules.Desynthesizable, NotifyChanged);
AddNode(_desynthFilter);
_glamourFilter = new StateFilterRowNode("Glamourable", CategoryDefinition.Rules.Glamourable, NotifyChanged);
AddNode(_glamourFilter);
_spiritbondFilter = new StateFilterRowNode("Spiritbonded", CategoryDefinition.Rules.FullySpiritbonded, NotifyChanged);
AddNode(_spiritbondFilter);
AddNode(CreateSectionHeader("List Filters"));
_allowedItemIdsEditor = new UintListEditorNode(
"Allowed Item IDs:",
CategoryDefinition.Rules.AllowedItemIds,
OnListChanged,
ResolveItemName
);
AddNode(_allowedItemIdsEditor);
_allowedNamePatternsEditor = new StringListEditorNode(
"Name Patterns (Regex):",
CategoryDefinition.Rules.AllowedItemNamePatterns,
OnListChanged
);
AddNode(_allowedNamePatternsEditor);
_allowedUiCategoriesEditor = new UintListEditorNode(
"UI Categories:",
CategoryDefinition.Rules.AllowedUiCategoryIds,
OnListChanged,
ResolveUiCategoryName
);
AddNode(_allowedUiCategoriesEditor);
_allowedRaritiesEditor = new RarityEditorNode(
CategoryDefinition.Rules.AllowedRarities,
NotifyChanged
);
AddNode(_allowedRaritiesEditor);
_isInitialized = true;
}
private void OnListChanged()
{
NotifyChanged();
RecalculateLayout();
OnLayoutChanged?.Invoke();
}
private static string ResolveItemName(uint itemId)
{
try
{
var item = _sItemSheet?.GetRow(itemId);
return item?.Name.ToString() ?? "Unknown";
}
catch
{
return "Unknown";
}
}
private static string ResolveUiCategoryName(uint categoryId)
{
try
{
var category = _sUICategorySheet?.GetRow(categoryId);
return category?.Name.ToString() ?? "Unknown";
}
catch
{
return "Unknown";
}
}
private static void NotifyChanged()
{
InventoryOrchestrator.RefreshAll(updateMaps: true);
}
private void NotifyCategoryPropertyChanged()
{
OnCategoryPropertyChanged?.Invoke();
}
private static LabelTextNode CreateSectionHeader(string text)
{
return new LabelTextNode
{
Size = new Vector2(300, 22),
String = text,
TextColor = ColorHelper.GetColor(2),
TextOutlineColor = ColorHelper.GetColor(0),
}; };
} AddNode(_itemLevelFilter);
private (CheckboxNode enabled, NumericInputNode min, NumericInputNode max) CreateRangeFilter( _vendorPriceFilter = new RangeFilterRowUint
string label,
RangeFilter<int> filter,
int minBound,
int maxBound,
Action<bool, int, int> onUpdate)
{
var enabledCheckbox = new CheckboxNode
{ {
Size = new Vector2(200, 20), Label = "Vendor Price",
String = $"{label} Filter", MinBound = 0,
IsChecked = filter.Enabled, 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(enabledCheckbox); AddNode(_vendorPriceFilter);
var minNode = new NumericInputNode
{
Size = new Vector2(120, 28),
Min = minBound,
Max = maxBound,
Value = filter.Min,
IsEnabled = filter.Enabled,
};
var maxNode = new NumericInputNode
{
Size = new Vector2(120, 28),
Min = minBound,
Max = maxBound,
Value = filter.Max,
IsEnabled = filter.Enabled,
};
var rangeRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 8.0f };
rangeRow.AddNode(new LabelTextNode { TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(30, 28), String = "Min:" });
rangeRow.AddNode(minNode);
rangeRow.AddNode(new LabelTextNode { TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(30, 28), String = "Max:" });
rangeRow.AddNode(maxNode);
AddNode(rangeRow);
enabledCheckbox.OnClick = isChecked =>
{
minNode.IsEnabled = isChecked;
maxNode.IsEnabled = isChecked;
onUpdate(isChecked, minNode.Value, maxNode.Value);
};
minNode.OnValueUpdate = val => onUpdate(enabledCheckbox.IsChecked, val, maxNode.Value);
maxNode.OnValueUpdate = val => onUpdate(enabledCheckbox.IsChecked, minNode.Value, val);
return (enabledCheckbox, minNode, maxNode);
}
private (CheckboxNode enabled, NumericInputNode min, NumericInputNode max) CreateRangeFilterUint(
string label,
RangeFilter<uint> filter,
int minBound,
int maxBound)
{
var enabledCheckbox = new CheckboxNode
{
Size = new Vector2(200, 20),
String = $"{label} Filter",
IsChecked = filter.Enabled,
};
AddNode(enabledCheckbox);
var minNode = new NumericInputNode
{
Size = new Vector2(120, 28),
Min = minBound,
Max = maxBound,
Value = (int)filter.Min,
IsEnabled = filter.Enabled,
};
var maxNode = new NumericInputNode
{
Size = new Vector2(120, 28),
Min = minBound,
Max = maxBound,
Value = (int)Math.Min(filter.Max, maxBound),
IsEnabled = filter.Enabled,
};
var rangeRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 8.0f };
rangeRow.AddNode(new LabelTextNode { TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(30, 28), String = "Min:" });
rangeRow.AddNode(minNode);
rangeRow.AddNode(new LabelTextNode { TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(30, 28), String = "Max:" });
rangeRow.AddNode(maxNode);
AddNode(rangeRow);
enabledCheckbox.OnClick = isChecked =>
{
minNode.IsEnabled = isChecked;
maxNode.IsEnabled = isChecked;
CategoryDefinition.Rules.VendorPrice.Enabled = isChecked;
NotifyChanged();
};
minNode.OnValueUpdate = value =>
{
CategoryDefinition.Rules.VendorPrice.Min = (uint)value;
NotifyChanged();
};
maxNode.OnValueUpdate = value =>
{
CategoryDefinition.Rules.VendorPrice.Max = (uint)value;
NotifyChanged();
};
return (enabledCheckbox, minNode, maxNode);
}
public void SetCategory(UserCategoryDefinition newCategory)
{
CategoryDefinition = newCategory;
RefreshValues();
}
private void RefreshValues()
{
if (! _isInitialized) return;
_enabledCheckbox.IsChecked = CategoryDefinition.Enabled;
_pinnedCheckbox.IsChecked = CategoryDefinition.Pinned;
_colorInputNode.CurrentColor = CategoryDefinition.Color;
_nameInputNode.String = CategoryDefinition.Name;
_descriptionInputNode.String = CategoryDefinition.Description;
_priorityInputNode.Value = CategoryDefinition.Priority;
_orderInputNode.Value = CategoryDefinition.Order;
RefreshRangeFilter(_levelEnabledCheckbox, _levelMinNode, _levelMaxNode, CategoryDefinition.Rules.Level);
RefreshRangeFilter(_itemLevelEnabledCheckbox, _itemLevelMinNode, _itemLevelMaxNode, CategoryDefinition.Rules.ItemLevel);
_vendorPriceEnabledCheckbox.IsChecked = CategoryDefinition.Rules.VendorPrice.Enabled;
_vendorPriceMinNode.Value = (int)CategoryDefinition.Rules.VendorPrice.Min;
_vendorPriceMaxNode.Value = (int)Math.Min(CategoryDefinition.Rules.VendorPrice.Max, int.MaxValue);
_vendorPriceMinNode.IsEnabled = CategoryDefinition.Rules.VendorPrice.Enabled;
_vendorPriceMaxNode.IsEnabled = CategoryDefinition.Rules.VendorPrice.Enabled;
_untradableFilter.SetState(CategoryDefinition.Rules.Untradable);
_uniqueFilter.SetState(CategoryDefinition.Rules.Unique);
_collectableFilter.SetState(CategoryDefinition.Rules.Collectable);
_dyeableFilter.SetState(CategoryDefinition.Rules.Dyeable);
_repairableFilter.SetState(CategoryDefinition.Rules.Repairable);
_allowedItemIdsEditor.SetList(CategoryDefinition.Rules.AllowedItemIds);
_allowedNamePatternsEditor.SetList(CategoryDefinition.Rules.AllowedItemNamePatterns);
_allowedUiCategoriesEditor.SetList(CategoryDefinition.Rules.AllowedUiCategoryIds);
_allowedRaritiesEditor.SetList(CategoryDefinition.Rules.AllowedRarities);
RecalculateLayout(); RecalculateLayout();
OnLayoutChanged?.Invoke();
} }
private static void RefreshRangeFilter(CheckboxNode enabled, NumericInputNode min, NumericInputNode max, RangeFilter<int> filter) public void Refresh()
{ {
enabled.IsChecked = filter.Enabled; EnsureInitialized();
min.Value = filter.Min;
max.Value = filter.Max; _levelFilter!.SetFilter(CategoryDefinition.Rules.Level);
min.IsEnabled = filter.Enabled; _itemLevelFilter!.SetFilter(CategoryDefinition.Rules.ItemLevel);
max.IsEnabled = filter.Enabled; _vendorPriceFilter!.SetFilter(CategoryDefinition.Rules.VendorPrice);
RecalculateLayout();
ParentTreeListNode?.RefreshLayout();
}
}
public sealed class StateFiltersSection : ConfigurationSection
{
private readonly List<(StateFilterRowNode Node, Func<UserCategoryDefinition, StateFilter> GetFilter)> _filters = [];
private bool _initialized;
public StateFiltersSection(Func<UserCategoryDefinition> 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<UserCategoryDefinition, StateFilter> 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<UserCategoryDefinition> 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();
} }
} }
@@ -0,0 +1,206 @@
using System;
using System.Numerics;
using AetherBags.Configuration;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.Category;
public sealed class RangeFilterRow : VerticalListNode
{
private readonly CheckboxNode _enabledCheckbox;
private readonly NumericInputNode _minNode;
private readonly NumericInputNode _maxNode;
public Action<bool, int, int>? OnFilterChanged { get; set; }
public required string Label
{
get => _enabledCheckbox.String.Replace(" Filter", "");
init => _enabledCheckbox.String = $"{value} Filter";
}
public int MinBound
{
get => _minNode.Min;
init
{
_minNode.Min = value;
_maxNode.Min = value;
}
}
public int MaxBound
{
get => _minNode.Max;
init
{
_minNode.Max = value;
_maxNode.Max = value;
}
}
public RangeFilterRow()
{
FitContents = true;
ItemSpacing = 2.0f;
_enabledCheckbox = new CheckboxNode
{
Size = new Vector2(200, 20),
OnClick = isChecked =>
{
if (_minNode == null || _maxNode == null) return;
_minNode.IsEnabled = isChecked;
_maxNode.IsEnabled = isChecked;
OnFilterChanged?.Invoke(isChecked, _minNode.Value, _maxNode.Value);
},
};
AddNode(_enabledCheckbox);
var rangeRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 8.0f };
rangeRow.AddNode(new LabelTextNode
{
TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(30, 28),
String = "Min:",
});
_minNode = new NumericInputNode
{
Size = new Vector2(100, 28),
OnValueUpdate = val =>
{
if (_maxNode != null) OnFilterChanged?.Invoke(_enabledCheckbox.IsChecked, val, _maxNode.Value);
},
};
rangeRow.AddNode(_minNode);
rangeRow.AddNode(new LabelTextNode
{
TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(30, 28),
String = "Max:",
});
_maxNode = new NumericInputNode
{
Size = new Vector2(100, 28),
OnValueUpdate = val => OnFilterChanged?.Invoke(_enabledCheckbox.IsChecked, _minNode.Value, val),
};
rangeRow.AddNode(_maxNode);
AddNode(rangeRow);
}
public void SetFilter(RangeFilter<int> filter)
{
_enabledCheckbox.IsChecked = filter.Enabled;
_minNode.Value = filter.Min;
_maxNode.Value = filter.Max;
_minNode.IsEnabled = filter.Enabled;
_maxNode.IsEnabled = filter.Enabled;
}
}
public sealed class RangeFilterRowUint : VerticalListNode
{
private readonly CheckboxNode _enabledCheckbox;
private readonly NumericInputNode _minNode;
private readonly NumericInputNode _maxNode;
private int _maxBound = int.MaxValue;
public Action<bool, uint, uint>? OnFilterChanged { get; set; }
public required string Label
{
get => _enabledCheckbox.String.Replace(" Filter", "");
init => _enabledCheckbox.String = $"{value} Filter";
}
public int MinBound
{
get => _minNode.Min;
init
{
_minNode.Min = value;
_maxNode.Min = value;
}
}
public int MaxBound
{
get => _maxBound;
init
{
_maxBound = value;
_minNode.Max = value;
_maxNode.Max = value;
}
}
public RangeFilterRowUint()
{
FitContents = true;
ItemSpacing = 2.0f;
_enabledCheckbox = new CheckboxNode
{
Size = new Vector2(200, 20),
OnClick = isChecked =>
{
if (_minNode == null || _maxNode == null) return;
_minNode.IsEnabled = isChecked;
_maxNode.IsEnabled = isChecked;
OnFilterChanged?.Invoke(isChecked, (uint)_minNode.Value, (uint)_maxNode.Value);
},
};
AddNode(_enabledCheckbox);
var rangeRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 8.0f };
rangeRow.AddNode(new LabelTextNode
{
TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(30, 28),
String = "Min:",
});
_minNode = new NumericInputNode
{
Size = new Vector2(100, 28),
OnValueUpdate = val =>
{
if (_maxNode != null)
OnFilterChanged?.Invoke(_enabledCheckbox.IsChecked, (uint)val, (uint)_maxNode.Value);
},
};
rangeRow.AddNode(_minNode);
rangeRow.AddNode(new LabelTextNode
{
TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(30, 28),
String = "Max:",
});
_maxNode = new NumericInputNode
{
Size = new Vector2(100, 28),
OnValueUpdate = val => OnFilterChanged?.Invoke(_enabledCheckbox.IsChecked, (uint)_minNode.Value, (uint)val),
};
rangeRow.AddNode(_maxNode);
AddNode(rangeRow);
}
public void SetFilter(RangeFilter<uint> filter)
{
_enabledCheckbox.IsChecked = filter.Enabled;
_minNode.Value = (int)filter.Min;
_maxNode.Value = (int)Math.Min(filter.Max, _maxBound);
_minNode.IsEnabled = filter.Enabled;
_maxNode.IsEnabled = filter.Enabled;
}
}
@@ -1,25 +1,33 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.Category; namespace AetherBags.Nodes.Configuration.Category;
public sealed class RarityEditorNode : VerticalListNode public sealed class RarityEditorNode :VerticalListNode
{ {
private static readonly string[] RarityNames = { "Common (White)", "Uncommon (Green)", "Rare (Blue)", "Relic (Purple)", "Aetherial (Pink)" }; private const float LabelWidth = 120f;
private const float CheckboxWidth = 150f;
private List<int> _list; private static readonly string[] RarityNames =
private readonly List<CheckboxNode> _checkboxes = new(); [
private readonly Action? _onChanged; "Common (White)",
"Uncommon (Green)",
"Rare (Blue)",
"Relic (Purple)",
"Aetherial (Pink)"
];
public RarityEditorNode(List<int> list, Action? onChanged = null) public Action? OnChanged { get; set; }
private List<int> _list = [];
private readonly List<CheckboxNode> _checkboxes = [];
public RarityEditorNode()
{ {
_list = list;
_onChanged = onChanged;
FitContents = true; FitContents = true;
ItemSpacing = 2.0f; ItemSpacing = 2.0f;
@@ -32,33 +40,35 @@ public sealed class RarityEditorNode : VerticalListNode
}; };
AddNode(headerLabel); AddNode(headerLabel);
for (int i = 0; i < RarityNames.Length; i++) for (var i = 0; i < RarityNames.Length; i++)
{ {
var rarity = i; var rarity = i;
var checkbox = new CheckboxNode var checkbox = new CheckboxNode
{ {
Size = new Vector2(200, 20), Size = new Vector2(LabelWidth + CheckboxWidth, 22),
String = RarityNames[i], String = RarityNames[i],
IsChecked = _list.Contains(i), OnClick = isChecked => ToggleRarity(rarity, isChecked),
OnClick = isChecked =>
{
if (isChecked && !_list.Contains(rarity))
{
_list.Add(rarity);
_list.Sort();
}
else if (!isChecked && _list.Contains(rarity))
{
_list.Remove(rarity);
}
_onChanged?.Invoke();
},
}; };
_checkboxes.Add(checkbox); _checkboxes.Add(checkbox);
AddNode(checkbox); AddNode(checkbox);
} }
} }
private void ToggleRarity(int rarity, bool isChecked)
{
if (isChecked && !_list.Contains(rarity))
{
_list.Add(rarity);
_list.Sort();
}
else if (!isChecked && _list.Contains(rarity))
{
_list.Remove(rarity);
}
OnChanged?.Invoke();
}
public void SetList(List<int> newList) public void SetList(List<int> newList)
{ {
_list = newList; _list = newList;
@@ -67,7 +77,7 @@ public sealed class RarityEditorNode : VerticalListNode
public void Refresh() public void Refresh()
{ {
for (int i = 0; i < _checkboxes.Count; i++) for (var i = 0; i < _checkboxes.Count; i++)
{ {
_checkboxes[i].IsChecked = _list.Contains(i); _checkboxes[i].IsChecked = _list.Contains(i);
} }
@@ -2,6 +2,7 @@ using AetherBags.Configuration;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes; using KamiToolKit.Classes;
using KamiToolKit.Nodes; using KamiToolKit.Nodes;
using KamiToolKit.Premade.Nodes;
using System; using System;
using System.Numerics; using System.Numerics;
@@ -9,48 +10,54 @@ namespace AetherBags.Nodes.Configuration.Category;
public sealed class StateFilterRowNode : HorizontalListNode public sealed class StateFilterRowNode : HorizontalListNode
{ {
private readonly LabelTextNode _labelNode; private const float LabelWidth = 120f;
private readonly TextButtonNode _stateButton; private const float ButtonWidth = 100f;
private readonly StateFilterButton _stateButton;
private readonly Action? _onChanged; private readonly Action? _onChanged;
private StateFilter _filter; private StateFilter _filter;
private static readonly string[] StateLabels = { "Ignored", "Allow", "Disallow" }; public StateFilterRowNode(string label, StateFilter filter, Action?onChanged = null)
public StateFilterRowNode(string label, StateFilter filter, Action? onChanged = null)
{ {
_filter = filter; _filter = filter;
_onChanged = onChanged; _onChanged = onChanged;
Size = new Vector2(280, 24); Size = new Vector2(LabelWidth + ButtonWidth + 8f, 24);
ItemSpacing = 8.0f; ItemSpacing = 8.0f;
_labelNode = new LabelTextNode var labelNode = new LabelTextNode
{ {
TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(LabelWidth, 24),
Size = new Vector2(100, 24),
String = $"{label}:", String = $"{label}:",
TextColor = ColorHelper.GetColor(8), TextColor = ColorHelper.GetColor(8),
AlignmentType = AlignmentType.Right,
}; };
AddNode(_labelNode); AddNode(labelNode);
_stateButton = new TextButtonNode _stateButton = new StateFilterButton
{ {
Size = new Vector2(100, 24), Size = new Vector2(ButtonWidth, 24),
String = StateLabels[_filter.State], States = [0, 1, 2],
OnClick = CycleState, SelectedState = _filter.State,
OnStateChanged = newState =>
{
_filter.State = newState;
_onChanged?.Invoke();
}
}; };
AddNode(_stateButton); AddNode(_stateButton);
} }
private void CycleState()
{
_filter.State = (_filter.State + 1) % 3;
_stateButton.String = StateLabels[_filter.State];
_onChanged?.Invoke();
}
public void SetState(StateFilter newFilter) public void SetState(StateFilter newFilter)
{ {
_filter = newFilter; _filter = newFilter;
_stateButton.String = StateLabels[_filter.State]; _stateButton.SelectedState = _filter.State;
}
private sealed class StateFilterButton : MultiStateButtonNode<int>
{
private static readonly string[] StateLabels = ["Ignored", "Required", "Excluded"];
protected override string GetStateText(int state)
=> state >= 0 && state < StateLabels.Length ?StateLabels[state] : "Unknown";
} }
} }
@@ -1,84 +1,77 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.Category; namespace AetherBags.Nodes.Configuration.Category;
public sealed class StringListEditorNode : VerticalListNode public sealed class StringListEditorNode : VerticalListNode
{ {
private List<string> _list; private const float LabelWidth = 300f;
private readonly TextInputNode _addInput; private const float RowHeight = 28f;
private List<string> _list = [];
private readonly LabelTextNode _headerLabel;
private readonly VerticalListNode _itemsContainer; private readonly VerticalListNode _itemsContainer;
private readonly Action? _onChanged; private readonly HorizontalListNode _addRow;
private readonly TextInputNode _addInput;
public StringListEditorNode(string label, List<string> list, Action? onChanged = null) public Action? OnChanged { get; set; }
public required string Label
{ {
_list = list; get => _headerLabel.String;
_onChanged = onChanged; init => _headerLabel.String = value;
}
public StringListEditorNode()
{
FitContents = true; FitContents = true;
ItemSpacing = 4.0f; ItemSpacing = 4.0f;
var headerLabel = new LabelTextNode _headerLabel = new LabelTextNode
{ {
TextFlags = TextFlags.AutoAdjustNodeSize, TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(280, 18), Size = new Vector2(280, 18),
String = label,
TextColor = ColorHelper.GetColor(8), TextColor = ColorHelper.GetColor(8),
}; };
AddNode(headerLabel); AddNode(_headerLabel);
_itemsContainer = new VerticalListNode _itemsContainer = new VerticalListNode
{ {
FitContents = true, Size = new Vector2(LabelWidth + 40f, 0),
ItemSpacing = 2.0f, ItemSpacing = 2.0f,
FitContents = true,
FirstItemSpacing = 2,
}; };
AddNode(_itemsContainer); AddNode(_itemsContainer);
var addRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 4.0f }; _addRow = new HorizontalListNode
{
Size = new Vector2(LabelWidth + 40f, RowHeight),
ItemSpacing = 4.0f,
};
_addInput = new TextInputNode _addInput = new TextInputNode
{ {
Size = new Vector2(200, 28), Size = new Vector2(200, RowHeight),
PlaceholderString = "Add new...", PlaceholderString = "Add new...",
OnInputComplete = text => OnInputComplete = _ => AddCurrentValue(),
{
var value = text.ExtractText();
if (!string.IsNullOrWhiteSpace(value) && ! _list.Contains(value))
{
_list.Add(value);
_addInput?.String = "";
RefreshItems();
_onChanged?.Invoke();
}
},
}; };
addRow.AddNode(_addInput); _addRow.AddNode(_addInput);
var addButton = new TextButtonNode var addButton = new TextButtonNode
{ {
Size = new Vector2(60, 28), Size = new Vector2(60, RowHeight),
String = "Add", String = "Add",
OnClick = () => OnClick = AddCurrentValue,
{
var value = _addInput.String;
if (!string.IsNullOrWhiteSpace(value) && !_list.Contains(value))
{
_list.Add(value);
_addInput.String = "";
RefreshItems();
_onChanged?.Invoke();
}
},
}; };
addRow.AddNode(addButton); _addRow.AddNode(addButton);
AddNode(addRow); AddNode(_addRow);
RefreshItems();
} }
public void SetList(List<string> newList) public void SetList(List<string> newList)
@@ -87,35 +80,54 @@ public sealed class StringListEditorNode : VerticalListNode
RefreshItems(); RefreshItems();
} }
private void AddCurrentValue()
{
var value = _addInput.String;
if (!string.IsNullOrWhiteSpace(value) && !_list.Contains(value))
{
_list.Add(value);
_addInput.String = "";
RefreshItems();
OnChanged?.Invoke();
}
}
private void RefreshItems() private void RefreshItems()
{ {
_itemsContainer.SyncWithListData( _itemsContainer.Clear();
_list,
node => node.Value, foreach (var value in _list)
value => new StringListItemNode(value) {
{ _itemsContainer.AddNode(CreateItemNode(value));
Size = new Vector2(280, 22), }
OnRemove = () =>
{ if (_list.Count == 0)
_list.Remove(value); {
RefreshItems(); _itemsContainer.Height = 0;
_onChanged?.Invoke(); }
},
}
);
_itemsContainer.RecalculateLayout(); _itemsContainer.RecalculateLayout();
RecalculateLayout(); RecalculateLayout();
} }
public void Refresh() private StringListItemNode CreateItemNode(string value) => new(value)
{ {
Size = new Vector2(LabelWidth + 40f, RowHeight),
OnRemove = () => RemoveValue(value),
};
private void RemoveValue(string value)
{
_list.Remove(value);
RefreshItems(); RefreshItems();
OnChanged?.Invoke();
} }
} }
public sealed class StringListItemNode : HorizontalListNode public sealed class StringListItemNode : HorizontalListNode
{ {
private const float LabelWidth = 300f;
public string Value { get; } public string Value { get; }
public Action? OnRemove { get; init; } public Action? OnRemove { get; init; }
@@ -124,20 +136,18 @@ public sealed class StringListItemNode : HorizontalListNode
Value = value; Value = value;
ItemSpacing = 4.0f; ItemSpacing = 4.0f;
var itemLabel = new LabelTextNode AddNode(new LabelTextNode
{ {
Size = new Vector2(220, 22), Size = new Vector2(LabelWidth, 24),
String = value, String = value,
TextColor = ColorHelper.GetColor(3), TextColor = ColorHelper.GetColor(3),
}; });
AddNode(itemLabel);
var removeButton = new TextButtonNode AddNode(new CircleButtonNode
{ {
Size = new Vector2(50, 22), Size = new Vector2(28, 28),
String = "X", Icon = ButtonIcon.Cross,
OnClick = () => OnRemove?.Invoke(), OnClick = () => OnRemove?.Invoke(),
}; });
AddNode(removeButton);
} }
} }
@@ -1,76 +1,79 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.Category; namespace AetherBags.Nodes.Configuration.Category;
public sealed class UintListEditorNode : VerticalListNode public sealed class UintListEditorNode : VerticalListNode
{ {
private List<uint> _list; private const float LabelWidth = 300f;
private readonly NumericInputNode _addInput; private const float RowHeight = 28f;
private List<uint> _list = [];
private readonly LabelTextNode _headerLabel;
private readonly VerticalListNode _itemsContainer; private readonly VerticalListNode _itemsContainer;
private readonly Action? _onChanged; private readonly HorizontalListNode _addRow;
private readonly Func<uint, string>? _labelResolver; private readonly NumericInputNode _addInput;
public UintListEditorNode(string label, List<uint> list, Action? onChanged = null, Func<uint, string>? labelResolver = null) public Func<uint, string>? LabelResolver { get; init; }
public Action? OnChanged { get; set; }
public required string Label
{ {
_list = list; get => _headerLabel.String;
_onChanged = onChanged; init => _headerLabel.String = value;
_labelResolver = labelResolver; }
public UintListEditorNode()
{
FitContents = true; FitContents = true;
ItemSpacing = 4.0f; ItemSpacing = 4.0f;
var headerLabel = new LabelTextNode _headerLabel = new LabelTextNode
{ {
TextFlags = TextFlags.AutoAdjustNodeSize, TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(280, 18), Size = new Vector2(280, 18),
String = label,
TextColor = ColorHelper.GetColor(8), TextColor = ColorHelper.GetColor(8),
}; };
AddNode(headerLabel); AddNode(_headerLabel);
_itemsContainer = new VerticalListNode _itemsContainer = new VerticalListNode
{ {
FitContents = true, Size = new Vector2(LabelWidth + 40f, 0),
ItemSpacing = 2.0f, ItemSpacing = 2.0f,
FitContents = true,
FirstItemSpacing = 2,
}; };
AddNode(_itemsContainer); AddNode(_itemsContainer);
var addRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 4.0f }; _addRow = new HorizontalListNode
{
Size = new Vector2(LabelWidth + 40f, RowHeight),
ItemSpacing = 4.0f,
};
_addInput = new NumericInputNode _addInput = new NumericInputNode
{ {
Size = new Vector2(120, 28), Size = new Vector2(120, RowHeight),
Min = 0, Min = 0,
Max = int.MaxValue, Max = int.MaxValue,
Value = 0, Value = 0,
}; };
addRow.AddNode(_addInput); _addRow.AddNode(_addInput);
var addButton = new TextButtonNode var addButton = new TextButtonNode
{ {
Size = new Vector2(60, 28), Size = new Vector2(60, RowHeight),
String = "Add", String = "Add",
OnClick = () => OnClick = AddCurrentValue,
{
var value = (uint)_addInput.Value;
if (! _list.Contains(value))
{
_list.Add(value);
RefreshItems();
_onChanged?.Invoke();
}
},
}; };
addRow.AddNode(addButton); _addRow.AddNode(addButton);
AddNode(addRow); AddNode(_addRow);
RefreshItems();
} }
public void SetList(List<uint> newList) public void SetList(List<uint> newList)
@@ -79,35 +82,53 @@ public sealed class UintListEditorNode : VerticalListNode
RefreshItems(); RefreshItems();
} }
private void AddCurrentValue()
{
var value = (uint)_addInput.Value;
if (!_list.Contains(value))
{
_list.Add(value);
RefreshItems();
OnChanged?.Invoke();
}
}
private void RefreshItems() private void RefreshItems()
{ {
_itemsContainer.SyncWithListData( _itemsContainer.Clear();
_list,
node => node.Value, foreach (var value in _list)
value => new UintListItemNode(value, _labelResolver) {
{ _itemsContainer.AddNode(CreateItemNode(value));
Size = new Vector2(280, 22), }
OnRemove = () =>
{ if (_list.Count == 0)
_list.Remove(value); {
RefreshItems(); _itemsContainer.Height = 0;
_onChanged?.Invoke(); }
},
}
);
_itemsContainer.RecalculateLayout(); _itemsContainer.RecalculateLayout();
RecalculateLayout(); RecalculateLayout();
} }
public void Refresh() private UintListItemNode CreateItemNode(uint value) => new(value, LabelResolver)
{ {
Size = new Vector2(LabelWidth + 40f, RowHeight),
OnRemove = () => RemoveValue(value),
};
private void RemoveValue(uint value)
{
_list.Remove(value);
RefreshItems(); RefreshItems();
OnChanged?.Invoke();
} }
} }
public sealed class UintListItemNode : HorizontalListNode public sealed class UintListItemNode : HorizontalListNode
{ {
private const float LabelWidth = 300f;
public uint Value { get; } public uint Value { get; }
public Action? OnRemove { get; init; } public Action? OnRemove { get; init; }
@@ -116,22 +137,22 @@ public sealed class UintListItemNode : HorizontalListNode
Value = value; Value = value;
ItemSpacing = 4.0f; ItemSpacing = 4.0f;
var displayText = labelResolver != null ? $"{value} - {labelResolver(value)}" : value.ToString(); var displayText = labelResolver is not null
var itemLabel = new LabelTextNode ? $"{value} - {labelResolver(value)}"
: value.ToString();
AddNode(new LabelTextNode
{ {
TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(LabelWidth, 24),
Size = new Vector2(220, 22),
String = displayText, String = displayText,
TextColor = ColorHelper.GetColor(3), TextColor = ColorHelper.GetColor(3),
}; });
AddNode(itemLabel);
var removeButton = new TextButtonNode AddNode(new CircleButtonNode
{ {
Size = new Vector2(50, 22), Size = new Vector2(28, 28),
String = "X", Icon = ButtonIcon.Cross,
OnClick = () => OnRemove?.Invoke(), OnClick = () => OnRemove?.Invoke(),
}; });
AddNode(removeButton);
} }
} }
@@ -45,6 +45,7 @@ public sealed class ImportExportResetNode : HorizontalListNode
Height = 32, Height = 32,
Width = 100, Width = 100,
String = "Reset", String = "Reset",
TextNode = { TextColor = ColorHelper.GetColor(50) },
TextTooltip = " Reset configuration\n(hold button to confirm)", TextTooltip = " Reset configuration\n(hold button to confirm)",
OnClick = ResetConfig OnClick = ResetConfig
}); });