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
@@ -7,66 +7,52 @@ namespace AetherBags.Nodes.Configuration.Category;
public class CategoryConfigurationNode : ConfigNode<CategoryWrapper>
{
private readonly ScrollingListNode _categoryList;
private CategoryDefinitionConfigurationNode? _activeNode;
public Action? OnCategoryChanged { get; set; }
public CategoryConfigurationNode()
{
_categoryList = new ScrollingListNode
{
AutoHideScrollBar = true,
};
_categoryList.FitContents = true;
_categoryList.AttachNode(this);
}
protected override void OptionChanged(CategoryWrapper? option)
{
if (option?.CategoryDefinition is null)
{
_categoryList.IsVisible = false;
if (_activeNode is not null)
{
_activeNode.IsVisible = false;
}
return;
}
_categoryList.IsVisible = true;
if (_activeNode is null)
{
_activeNode = new CategoryDefinitionConfigurationNode(option.CategoryDefinition)
_activeNode = new CategoryDefinitionConfigurationNode
{
Size = _categoryList.Size,
OnLayoutChanged = UpdateScrollHeight,
OnLayoutChanged = RecalculateLayout,
OnCategoryPropertyChanged = OnCategoryChanged,
};
_categoryList.AddNode(_activeNode);
_activeNode.AttachNode(this);
}
else
{
_activeNode.IsVisible = true;
_activeNode.Size = Size;
_activeNode.SetCategory(option.CategoryDefinition);
}
UpdateScrollHeight();
}
private void UpdateScrollHeight()
private void RecalculateLayout()
{
_categoryList.RecalculateLayout();
//_categoryList.ContentHeight = _categoryList.Height;
// Trigger parent layout update if needed
}
protected override void 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.Collections.Generic;
using System.Numerics;
using AetherBags.Configuration;
using AetherBags.Inventory;
@@ -15,520 +16,500 @@ using Action = System.Action;
namespace AetherBags.Nodes.Configuration.Category;
public sealed class CategoryDefinitionConfigurationNode : VerticalListNode
public sealed class CategoryDefinitionConfigurationNode : SimpleComponentNode
{
private readonly CheckboxNode _enabledCheckbox;
private readonly CheckboxNode _pinnedCheckbox;
private readonly TextInputNode _nameInputNode;
private readonly TextInputNode _descriptionInputNode;
private readonly ColorInputRow _colorInputNode;
private readonly NumericInputNode _priorityInputNode;
private readonly NumericInputNode _orderInputNode;
private static ExcelSheet<Item>? ItemSheet => Services.DataManager.GetExcelSheet<Item>();
private static ExcelSheet<ItemUICategory>? UICategorySheet => Services.DataManager.GetExcelSheet<ItemUICategory>();
private readonly CheckboxNode _levelEnabledCheckbox;
private readonly NumericInputNode _levelMinNode;
private readonly NumericInputNode _levelMaxNode;
public Action? OnLayoutChanged { get; init; }
public Action? OnCategoryPropertyChanged { get; init; }
private readonly CheckboxNode _itemLevelEnabledCheckbox;
private readonly NumericInputNode _itemLevelMinNode;
private readonly NumericInputNode _itemLevelMaxNode;
private UserCategoryDefinition _categoryDefinition = new();
private readonly CheckboxNode _vendorPriceEnabledCheckbox;
private readonly NumericInputNode _vendorPriceMinNode;
private readonly NumericInputNode _vendorPriceMaxNode;
private readonly ScrollingAreaNode<TreeListNode> _scrollingArea;
private readonly BasicSettingsSection _basicSettings;
private readonly RangeFiltersSection _rangeFilters;
private readonly StateFiltersSection _stateFilters;
private readonly ListFiltersSection _listFilters;
private readonly StateFilterRowNode _untradableFilter;
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)
public CategoryDefinitionConfigurationNode()
{
CategoryDefinition = categoryDefinition;
_sItemSheet ??= Services.DataManager.GetExcelSheet<Item>();
_sUICategorySheet ??= Services.DataManager.GetExcelSheet<ItemUICategory>();
FitContents = true;
ItemSpacing = 4.0f;
var catchAllWarningNode = new TextNode
_scrollingArea = new ScrollingAreaNode<TreeListNode>
{
Size = new Vector2(300, 40),
TextFlags = TextFlags.MultiLine | TextFlags.AutoAdjustNodeSize,
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),
ContentHeight = 100.0f,
AutoHideScrollBar = true,
};
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
{
Size = new Vector2(200, 20),
Size = new Vector2(Width, 20),
String = "Enabled",
IsChecked = CategoryDefinition.Enabled,
OnClick = isChecked =>
{
CategoryDefinition.Enabled = isChecked;
NotifyChanged();
NotifyCategoryPropertyChanged();
OnPropertyChanged?.Invoke();
},
};
AddNode(_enabledCheckbox);
_pinnedCheckbox = new CheckboxNode
{
Size = new Vector2(200, 20),
Size = new Vector2(Width, 20),
String = "Pinned",
IsChecked = CategoryDefinition.Pinned,
OnClick = isChecked =>
{
CategoryDefinition.Pinned = isChecked;
NotifyChanged();
NotifyCategoryPropertyChanged();
OnPropertyChanged?.Invoke();
},
};
AddNode(_pinnedCheckbox);
AddNode(new LabelTextNode
{
TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(80, 20),
String = "Name:"
});
_nameInputNode = new TextInputNode
AddNode(CreateLabel("Name: "));
_nameInput = new TextInputNode
{
Size = new Vector2(250, 28),
String = CategoryDefinition.Name,
PlaceholderString = CategoryDefinition.Name.IsNullOrWhitespace() ? "Category Name" : "",
OnInputReceived = name =>
PlaceholderString = "Category Name",
OnInputReceived = input =>
{
CategoryDefinition.Name = name.ExtractText();
NotifyChanged();
NotifyCategoryPropertyChanged();
CategoryDefinition.Name = input.ExtractText();
OnPropertyChanged?.Invoke();
},
};
AddNode(_nameInputNode);
AddNode(_nameInput);
AddNode(new LabelTextNode
{
TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(80, 20),
String = "Description:"
});
_descriptionInputNode = new TextInputNode
AddNode(CreateLabel("Description:"));
_descriptionInput = new TextInputNode
{
Size = new Vector2(250, 28),
String = CategoryDefinition.Description,
PlaceholderString = CategoryDefinition.Description.IsNullOrWhitespace() ? "Optional description" : "",
OnInputReceived = desc =>
PlaceholderString = "Optional description",
OnInputReceived = input =>
{
CategoryDefinition.Description = desc.ExtractText();
NotifyChanged();
CategoryDefinition.Description = input.ExtractText();
OnValueChanged?.Invoke();
},
};
AddNode(_descriptionInputNode);
AddNode(_descriptionInput);
_colorInputNode = new ColorInputRow
_colorInput = new ColorInputRow
{
Label = "Color",
Size = new Vector2(300, 28),
CurrentColor = CategoryDefinition.Color,
CurrentColor = new UserCategoryDefinition().Color,
DefaultColor = new UserCategoryDefinition().Color,
OnColorConfirmed = color =>
{
CategoryDefinition.Color = color;
NotifyChanged();
},
OnColorCanceled = color =>
{
CategoryDefinition.Color = color;
NotifyChanged();
},
OnColorPreviewed = color =>
{
CategoryDefinition.Color = color;
NotifyChanged();
}
OnColorConfirmed = c => { CategoryDefinition.Color = c; OnValueChanged?.Invoke(); },
OnColorCanceled = c => { CategoryDefinition.Color = c; OnValueChanged?.Invoke(); },
OnColorPreviewed = c => { CategoryDefinition.Color = c; OnValueChanged?.Invoke(); },
};
AddNode(_colorInputNode);
AddNode(_colorInput);
AddNode(new LabelTextNode
{
TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(80, 20),
String = "Priority:"
});
_priorityInputNode = new NumericInputNode
AddNode(CreateLabel("Priority:"));
_priorityInput = new NumericInputNode
{
Size = new Vector2(120, 28),
Min = 0,
Max = 1000,
Step = 1,
Value = CategoryDefinition.Priority,
OnValueUpdate = val =>
{
CategoryDefinition.Priority = val;
NotifyChanged();
OnValueChanged?.Invoke();
},
};
AddNode(_priorityInputNode);
AddNode(_priorityInput);
AddNode(new LabelTextNode { TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(80, 20), String = "Order:" });
_orderInputNode = new NumericInputNode
AddNode(CreateLabel("Order: "));
_orderInput = new NumericInputNode
{
Size = new Vector2(120, 28),
Min = 0,
Max = 9999,
Step = 1,
Value = CategoryDefinition.Order,
OnValueUpdate = val =>
{
CategoryDefinition.Order = val;
NotifyChanged();
NotifyCategoryPropertyChanged();
OnPropertyChanged?.Invoke();
},
};
AddNode(_orderInputNode);
AddNode(_orderInput);
AddNode(CreateSectionHeader("Range Filters"));
RecalculateLayout();
}
(_levelEnabledCheckbox, _levelMinNode, _levelMaxNode) = CreateRangeFilter(
"Level",
CategoryDefinition.Rules.Level,
0, 200,
(enabled, min, max) =>
public void Refresh()
{
EnsureInitialized();
_enabledCheckbox!.IsChecked = CategoryDefinition.Enabled;
_pinnedCheckbox!.IsChecked = CategoryDefinition.Pinned;
_nameInput!.String = CategoryDefinition.Name;
_nameInput.PlaceholderString = CategoryDefinition.Name.IsNullOrWhitespace() ? "Category Name" : "";
_descriptionInput!.String = CategoryDefinition.Description;
_descriptionInput.PlaceholderString = CategoryDefinition.Description.IsNullOrWhitespace() ? "Optional description" : "";
_colorInput!.CurrentColor = CategoryDefinition.Color;
_priorityInput!.Value = CategoryDefinition.Priority;
_orderInput!.Value = CategoryDefinition.Order;
RecalculateLayout();
ParentTreeListNode?.RefreshLayout();
}
}
public sealed class RangeFiltersSection : ConfigurationSection
{
private RangeFilterRow? _levelFilter;
private RangeFilterRow? _itemLevelFilter;
private RangeFilterRowUint? _vendorPriceFilter;
private bool _initialized;
public RangeFiltersSection(Func<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.Min = min;
CategoryDefinition.Rules.Level.Max = max;
NotifyChanged();
}
);
OnValueChanged?.Invoke();
},
};
AddNode(_levelFilter);
(_itemLevelEnabledCheckbox, _itemLevelMinNode, _itemLevelMaxNode) = CreateRangeFilter(
"Item Level",
CategoryDefinition.Rules.ItemLevel,
0, 2000,
(enabled, min, max) =>
_itemLevelFilter = new RangeFilterRow
{
Label = "Item Level",
MinBound = 0,
MaxBound = 2000,
OnFilterChanged = (enabled, min, max) =>
{
CategoryDefinition.Rules.ItemLevel.Enabled = enabled;
CategoryDefinition.Rules.ItemLevel.Min = min;
CategoryDefinition.Rules.ItemLevel.Max = max;
NotifyChanged();
}
);
(_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),
OnValueChanged?.Invoke();
},
};
}
AddNode(_itemLevelFilter);
private (CheckboxNode enabled, NumericInputNode min, NumericInputNode max) CreateRangeFilter(
string label,
RangeFilter<int> filter,
int minBound,
int maxBound,
Action<bool, int, int> onUpdate)
_vendorPriceFilter = new RangeFilterRowUint
{
var enabledCheckbox = new CheckboxNode
Label = "Vendor Price",
MinBound = 0,
MaxBound = 9_999_999,
OnFilterChanged = (enabled, min, max) =>
{
Size = new Vector2(200, 20),
String = $"{label} Filter",
IsChecked = filter.Enabled,
CategoryDefinition.Rules.VendorPrice.Enabled = enabled;
CategoryDefinition.Rules.VendorPrice.Min = min;
CategoryDefinition.Rules.VendorPrice.Max = max;
OnValueChanged?.Invoke();
},
};
AddNode(enabledCheckbox);
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);
AddNode(_vendorPriceFilter);
RecalculateLayout();
OnLayoutChanged?.Invoke();
}
private static void RefreshRangeFilter(CheckboxNode enabled, NumericInputNode min, NumericInputNode max, RangeFilter<int> filter)
public void Refresh()
{
enabled.IsChecked = filter.Enabled;
min.Value = filter.Min;
max.Value = filter.Max;
min.IsEnabled = filter.Enabled;
max.IsEnabled = filter.Enabled;
EnsureInitialized();
_levelFilter!.SetFilter(CategoryDefinition.Rules.Level);
_itemLevelFilter!.SetFilter(CategoryDefinition.Rules.ItemLevel);
_vendorPriceFilter!.SetFilter(CategoryDefinition.Rules.VendorPrice);
RecalculateLayout();
ParentTreeListNode?.RefreshLayout();
}
}
public sealed class StateFiltersSection : ConfigurationSection
{
private readonly List<(StateFilterRowNode Node, Func<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.Collections.Generic;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
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 readonly List<CheckboxNode> _checkboxes = new();
private readonly Action? _onChanged;
private static readonly string[] RarityNames =
[
"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;
ItemSpacing = 2.0f;
@@ -32,15 +40,21 @@ public sealed class RarityEditorNode : VerticalListNode
};
AddNode(headerLabel);
for (int i = 0; i < RarityNames.Length; i++)
for (var i = 0; i < RarityNames.Length; i++)
{
var rarity = i;
var checkbox = new CheckboxNode
{
Size = new Vector2(200, 20),
Size = new Vector2(LabelWidth + CheckboxWidth, 22),
String = RarityNames[i],
IsChecked = _list.Contains(i),
OnClick = isChecked =>
OnClick = isChecked => ToggleRarity(rarity, isChecked),
};
_checkboxes.Add(checkbox);
AddNode(checkbox);
}
}
private void ToggleRarity(int rarity, bool isChecked)
{
if (isChecked && !_list.Contains(rarity))
{
@@ -51,12 +65,8 @@ public sealed class RarityEditorNode : VerticalListNode
{
_list.Remove(rarity);
}
_onChanged?.Invoke();
},
};
_checkboxes.Add(checkbox);
AddNode(checkbox);
}
OnChanged?.Invoke();
}
public void SetList(List<int> newList)
@@ -67,7 +77,7 @@ public sealed class RarityEditorNode : VerticalListNode
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);
}
@@ -2,6 +2,7 @@ using AetherBags.Configuration;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
using KamiToolKit.Premade.Nodes;
using System;
using System.Numerics;
@@ -9,48 +10,54 @@ namespace AetherBags.Nodes.Configuration.Category;
public sealed class StateFilterRowNode : HorizontalListNode
{
private readonly LabelTextNode _labelNode;
private readonly TextButtonNode _stateButton;
private const float LabelWidth = 120f;
private const float ButtonWidth = 100f;
private readonly StateFilterButton _stateButton;
private readonly Action? _onChanged;
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;
_onChanged = onChanged;
Size = new Vector2(280, 24);
Size = new Vector2(LabelWidth + ButtonWidth + 8f, 24);
ItemSpacing = 8.0f;
_labelNode = new LabelTextNode
var labelNode = new LabelTextNode
{
TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(100, 24),
Size = new Vector2(LabelWidth, 24),
String = $"{label}:",
TextColor = ColorHelper.GetColor(8),
AlignmentType = AlignmentType.Right,
};
AddNode(_labelNode);
AddNode(labelNode);
_stateButton = new TextButtonNode
_stateButton = new StateFilterButton
{
Size = new Vector2(100, 24),
String = StateLabels[_filter.State],
OnClick = CycleState,
Size = new Vector2(ButtonWidth, 24),
States = [0, 1, 2],
SelectedState = _filter.State,
OnStateChanged = newState =>
{
_filter.State = newState;
_onChanged?.Invoke();
}
};
AddNode(_stateButton);
}
private void CycleState()
{
_filter.State = (_filter.State + 1) % 3;
_stateButton.String = StateLabels[_filter.State];
_onChanged?.Invoke();
}
public void SetState(StateFilter 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.Collections.Generic;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.Category;
public sealed class StringListEditorNode : VerticalListNode
{
private List<string> _list;
private readonly TextInputNode _addInput;
private const float LabelWidth = 300f;
private const float RowHeight = 28f;
private List<string> _list = [];
private readonly LabelTextNode _headerLabel;
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;
_onChanged = onChanged;
get => _headerLabel.String;
init => _headerLabel.String = value;
}
public StringListEditorNode()
{
FitContents = true;
ItemSpacing = 4.0f;
var headerLabel = new LabelTextNode
_headerLabel = new LabelTextNode
{
TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(280, 18),
String = label,
TextColor = ColorHelper.GetColor(8),
};
AddNode(headerLabel);
AddNode(_headerLabel);
_itemsContainer = new VerticalListNode
{
FitContents = true,
Size = new Vector2(LabelWidth + 40f, 0),
ItemSpacing = 2.0f,
FitContents = true,
FirstItemSpacing = 2,
};
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
{
Size = new Vector2(200, 28),
Size = new Vector2(200, RowHeight),
PlaceholderString = "Add new...",
OnInputComplete = text =>
{
var value = text.ExtractText();
if (!string.IsNullOrWhiteSpace(value) && ! _list.Contains(value))
{
_list.Add(value);
_addInput?.String = "";
RefreshItems();
_onChanged?.Invoke();
}
},
OnInputComplete = _ => AddCurrentValue(),
};
addRow.AddNode(_addInput);
_addRow.AddNode(_addInput);
var addButton = new TextButtonNode
{
Size = new Vector2(60, 28),
Size = new Vector2(60, RowHeight),
String = "Add",
OnClick = () =>
{
var value = _addInput.String;
if (!string.IsNullOrWhiteSpace(value) && !_list.Contains(value))
{
_list.Add(value);
_addInput.String = "";
RefreshItems();
_onChanged?.Invoke();
}
},
OnClick = AddCurrentValue,
};
addRow.AddNode(addButton);
_addRow.AddNode(addButton);
AddNode(addRow);
RefreshItems();
AddNode(_addRow);
}
public void SetList(List<string> newList)
@@ -87,35 +80,54 @@ public sealed class StringListEditorNode : VerticalListNode
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()
{
_itemsContainer.SyncWithListData(
_list,
node => node.Value,
value => new StringListItemNode(value)
_itemsContainer.Clear();
foreach (var value in _list)
{
Size = new Vector2(280, 22),
OnRemove = () =>
{
_list.Remove(value);
RefreshItems();
_onChanged?.Invoke();
},
_itemsContainer.AddNode(CreateItemNode(value));
}
if (_list.Count == 0)
{
_itemsContainer.Height = 0;
}
);
_itemsContainer.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();
OnChanged?.Invoke();
}
}
public sealed class StringListItemNode : HorizontalListNode
{
private const float LabelWidth = 300f;
public string Value { get; }
public Action? OnRemove { get; init; }
@@ -124,20 +136,18 @@ public sealed class StringListItemNode : HorizontalListNode
Value = value;
ItemSpacing = 4.0f;
var itemLabel = new LabelTextNode
AddNode(new LabelTextNode
{
Size = new Vector2(220, 22),
Size = new Vector2(LabelWidth, 24),
String = value,
TextColor = ColorHelper.GetColor(3),
};
AddNode(itemLabel);
});
var removeButton = new TextButtonNode
AddNode(new CircleButtonNode
{
Size = new Vector2(50, 22),
String = "X",
Size = new Vector2(28, 28),
Icon = ButtonIcon.Cross,
OnClick = () => OnRemove?.Invoke(),
};
AddNode(removeButton);
});
}
}
@@ -1,76 +1,79 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
using System;
using System.Collections.Generic;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.Category;
public sealed class UintListEditorNode : VerticalListNode
{
private List<uint> _list;
private readonly NumericInputNode _addInput;
private const float LabelWidth = 300f;
private const float RowHeight = 28f;
private List<uint> _list = [];
private readonly LabelTextNode _headerLabel;
private readonly VerticalListNode _itemsContainer;
private readonly Action? _onChanged;
private readonly Func<uint, string>? _labelResolver;
private readonly HorizontalListNode _addRow;
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;
_onChanged = onChanged;
_labelResolver = labelResolver;
get => _headerLabel.String;
init => _headerLabel.String = value;
}
public UintListEditorNode()
{
FitContents = true;
ItemSpacing = 4.0f;
var headerLabel = new LabelTextNode
_headerLabel = new LabelTextNode
{
TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(280, 18),
String = label,
TextColor = ColorHelper.GetColor(8),
};
AddNode(headerLabel);
AddNode(_headerLabel);
_itemsContainer = new VerticalListNode
{
FitContents = true,
Size = new Vector2(LabelWidth + 40f, 0),
ItemSpacing = 2.0f,
FitContents = true,
FirstItemSpacing = 2,
};
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
{
Size = new Vector2(120, 28),
Size = new Vector2(120, RowHeight),
Min = 0,
Max = int.MaxValue,
Value = 0,
};
addRow.AddNode(_addInput);
_addRow.AddNode(_addInput);
var addButton = new TextButtonNode
{
Size = new Vector2(60, 28),
Size = new Vector2(60, RowHeight),
String = "Add",
OnClick = () =>
{
var value = (uint)_addInput.Value;
if (! _list.Contains(value))
{
_list.Add(value);
RefreshItems();
_onChanged?.Invoke();
}
},
OnClick = AddCurrentValue,
};
addRow.AddNode(addButton);
_addRow.AddNode(addButton);
AddNode(addRow);
RefreshItems();
AddNode(_addRow);
}
public void SetList(List<uint> newList)
@@ -79,35 +82,53 @@ public sealed class UintListEditorNode : VerticalListNode
RefreshItems();
}
private void AddCurrentValue()
{
var value = (uint)_addInput.Value;
if (!_list.Contains(value))
{
_list.Add(value);
RefreshItems();
OnChanged?.Invoke();
}
}
private void RefreshItems()
{
_itemsContainer.SyncWithListData(
_list,
node => node.Value,
value => new UintListItemNode(value, _labelResolver)
_itemsContainer.Clear();
foreach (var value in _list)
{
Size = new Vector2(280, 22),
OnRemove = () =>
{
_list.Remove(value);
RefreshItems();
_onChanged?.Invoke();
},
_itemsContainer.AddNode(CreateItemNode(value));
}
if (_list.Count == 0)
{
_itemsContainer.Height = 0;
}
);
_itemsContainer.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();
OnChanged?.Invoke();
}
}
public sealed class UintListItemNode : HorizontalListNode
{
private const float LabelWidth = 300f;
public uint Value { get; }
public Action? OnRemove { get; init; }
@@ -116,22 +137,22 @@ public sealed class UintListItemNode : HorizontalListNode
Value = value;
ItemSpacing = 4.0f;
var displayText = labelResolver != null ? $"{value} - {labelResolver(value)}" : value.ToString();
var itemLabel = new LabelTextNode
var displayText = labelResolver is not null
? $"{value} - {labelResolver(value)}"
: value.ToString();
AddNode(new LabelTextNode
{
TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(220, 22),
Size = new Vector2(LabelWidth, 24),
String = displayText,
TextColor = ColorHelper.GetColor(3),
};
AddNode(itemLabel);
});
var removeButton = new TextButtonNode
AddNode(new CircleButtonNode
{
Size = new Vector2(50, 22),
String = "X",
Size = new Vector2(28, 28),
Icon = ButtonIcon.Cross,
OnClick = () => OnRemove?.Invoke(),
};
AddNode(removeButton);
});
}
}
@@ -45,6 +45,7 @@ public sealed class ImportExportResetNode : HorizontalListNode
Height = 32,
Width = 100,
String = "Reset",
TextNode = { TextColor = ColorHelper.GetColor(50) },
TextTooltip = " Reset configuration\n(hold button to confirm)",
OnClick = ResetConfig
});