From 3d9d399b26b08a32a748ed1029aee3da20af007f Mon Sep 17 00:00:00 2001 From: Zeffuro Date: Mon, 5 Jan 2026 03:01:37 +0100 Subject: [PATCH] Improve Category editor --- .../Category/CategoryConfigurationNode.cs | 44 +- .../CategoryDefinitionConfigurationNode.cs | 833 +++++++++--------- .../Configuration/Category/RangeFilterRow.cs | 206 +++++ .../Category/RarityEditorNode.cs | 68 +- .../Category/StateFilterRowNode.cs | 51 +- .../Category/StringListEditorNode.cs | 144 +-- .../Category/UintListEditorNode.cs | 141 +-- .../General/ImportExportResetNode.cs | 1 + 8 files changed, 855 insertions(+), 633 deletions(-) create mode 100644 AetherBags/Nodes/Configuration/Category/RangeFilterRow.cs diff --git a/AetherBags/Nodes/Configuration/Category/CategoryConfigurationNode.cs b/AetherBags/Nodes/Configuration/Category/CategoryConfigurationNode.cs index ca81a03..3296573 100644 --- a/AetherBags/Nodes/Configuration/Category/CategoryConfigurationNode.cs +++ b/AetherBags/Nodes/Configuration/Category/CategoryConfigurationNode.cs @@ -5,68 +5,54 @@ using KamiToolKit.Premade.Nodes; namespace AetherBags.Nodes.Configuration.Category; -public class CategoryConfigurationNode : ConfigNode +public class CategoryConfigurationNode : ConfigNode { - 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); - } - else - { - _activeNode.SetCategory(option.CategoryDefinition); + _activeNode.AttachNode(this); } - UpdateScrollHeight(); + _activeNode.IsVisible = true; + _activeNode.Size = Size; + _activeNode.SetCategory(option.CategoryDefinition); } - 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()) + if (_activeNode is not null) { - node.Width = Width; + _activeNode.Size = Size; } - - UpdateScrollHeight(); } } \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs b/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs index c801073..50f1eb8 100644 --- a/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs +++ b/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs @@ -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? ItemSheet => Services.DataManager.GetExcelSheet(); + private static ExcelSheet? UICategorySheet => Services.DataManager.GetExcelSheet(); - 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 _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? _sItemSheet; - private static ExcelSheet? _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(); - _sUICategorySheet ??= Services.DataManager.GetExcelSheet(); - - FitContents = true; - ItemSpacing = 4.0f; - - var catchAllWarningNode = new TextNode + _scrollingArea = new ScrollingAreaNode { - 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 _getCategoryDefinition; + + public Action? OnValueChanged { get; init; } + + protected UserCategoryDefinition CategoryDefinition => _getCategoryDefinition(); + + protected ConfigurationSection(Func 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 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 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 filter, - int minBound, - int maxBound, - Action onUpdate) - { - var enabledCheckbox = new CheckboxNode + _vendorPriceFilter = new RangeFilterRowUint { - Size = new Vector2(200, 20), - String = $"{label} Filter", - IsChecked = filter.Enabled, + Label = "Vendor Price", + MinBound = 0, + MaxBound = 9_999_999, + OnFilterChanged = (enabled, min, max) => + { + CategoryDefinition.Rules.VendorPrice.Enabled = enabled; + CategoryDefinition.Rules.VendorPrice.Min = min; + CategoryDefinition.Rules.VendorPrice.Max = max; + OnValueChanged?.Invoke(); + }, }; - AddNode(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 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 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 GetFilter)> _filters = []; + private bool _initialized; + + public StateFiltersSection(Func getCategoryDefinition) + : base(getCategoryDefinition) + { + } + + private void EnsureInitialized() + { + if (_initialized) return; + _initialized = true; + + AddFilter("Untradable", def => def.Rules.Untradable); + AddFilter("Unique", def => def.Rules.Unique); + AddFilter("Collectable", def => def.Rules.Collectable); + AddFilter("Dyeable", def => def.Rules.Dyeable); + AddFilter("Repairable", def => def.Rules.Repairable); + AddFilter("High Quality", def => def.Rules.HighQuality); + AddFilter("Desynthesizable", def => def.Rules.Desynthesizable); + AddFilter("Glamourable", def => def.Rules.Glamourable); + AddFilter("Spiritbonded", def => def.Rules.FullySpiritbonded); + + RecalculateLayout(); + } + + private void AddFilter(string label, Func getFilter) + { + var node = new StateFilterRowNode(label, new StateFilter(), () => OnValueChanged?.Invoke()); + _filters.Add((node, getFilter)); + AddNode(node); + } + + public void Refresh() + { + EnsureInitialized(); + + foreach (var (node, getFilter) in _filters) + { + node.SetState(getFilter(CategoryDefinition)); + } + + RecalculateLayout(); + ParentTreeListNode?.RefreshLayout(); + } +} + +public sealed class ListFiltersSection : ConfigurationSection +{ + public Action? OnListChanged { get; init; } + + private UintListEditorNode? _itemIdsEditor; + private StringListEditorNode? _namePatternsEditor; + private UintListEditorNode? _uiCategoriesEditor; + private RarityEditorNode? _raritiesEditor; + + private bool _initialized; + + public ListFiltersSection(Func getCategoryDefinition) + : base(getCategoryDefinition) + { + } + + private void EnsureInitialized() + { + if (_initialized) return; + _initialized = true; + + _itemIdsEditor = new UintListEditorNode + { + Label = "Allowed Item IDs:", + LabelResolver = CategoryDefinitionConfigurationNode.ResolveItemName, + OnChanged = () => + { + OnListChanged?.Invoke(); + RecalculateLayout(); + ParentTreeListNode?.RefreshLayout(); + }, + }; + AddNode(_itemIdsEditor); + + _namePatternsEditor = new StringListEditorNode + { + Label = "Name Patterns (Regex):", + OnChanged = () => + { + OnListChanged?.Invoke(); + RecalculateLayout(); + ParentTreeListNode?.RefreshLayout(); + }, + }; + AddNode(_namePatternsEditor); + + _uiCategoriesEditor = new UintListEditorNode + { + Label = "UI Categories:", + LabelResolver = CategoryDefinitionConfigurationNode.ResolveUiCategoryName, + OnChanged = () => + { + OnListChanged?.Invoke(); + RecalculateLayout(); + ParentTreeListNode?.RefreshLayout(); + }, + }; + AddNode(_uiCategoriesEditor); + + _raritiesEditor = new RarityEditorNode + { + OnChanged = () => OnValueChanged?.Invoke(), + }; + AddNode(_raritiesEditor); + + RecalculateLayout(); + } + + public void Refresh() + { + EnsureInitialized(); + + _itemIdsEditor!.SetList(CategoryDefinition.Rules.AllowedItemIds); + _namePatternsEditor!.SetList(CategoryDefinition.Rules.AllowedItemNamePatterns); + _uiCategoriesEditor!.SetList(CategoryDefinition.Rules.AllowedUiCategoryIds); + _raritiesEditor!.SetList(CategoryDefinition.Rules.AllowedRarities); + + RecalculateLayout(); + ParentTreeListNode?.RefreshLayout(); } } \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/RangeFilterRow.cs b/AetherBags/Nodes/Configuration/Category/RangeFilterRow.cs new file mode 100644 index 0000000..9304a42 --- /dev/null +++ b/AetherBags/Nodes/Configuration/Category/RangeFilterRow.cs @@ -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? 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 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? 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 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; + } +} \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/RarityEditorNode.cs b/AetherBags/Nodes/Configuration/Category/RarityEditorNode.cs index c100034..1dc10b4 100644 --- a/AetherBags/Nodes/Configuration/Category/RarityEditorNode.cs +++ b/AetherBags/Nodes/Configuration/Category/RarityEditorNode.cs @@ -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 _list; - private readonly List _checkboxes = new(); - private readonly Action? _onChanged; + private static readonly string[] RarityNames = + [ + "Common (White)", + "Uncommon (Green)", + "Rare (Blue)", + "Relic (Purple)", + "Aetherial (Pink)" + ]; - public RarityEditorNode(List list, Action? onChanged = null) + public Action? OnChanged { get; set; } + + private List _list = []; + private readonly List _checkboxes = []; + + public RarityEditorNode() { - _list = list; - _onChanged = onChanged; - FitContents = true; ItemSpacing = 2.0f; @@ -32,33 +40,35 @@ 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 => - { - if (isChecked && !_list.Contains(rarity)) - { - _list.Add(rarity); - _list.Sort(); - } - else if (!isChecked && _list.Contains(rarity)) - { - _list.Remove(rarity); - } - _onChanged?.Invoke(); - }, + OnClick = isChecked => ToggleRarity(rarity, isChecked), }; _checkboxes.Add(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 newList) { _list = 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); } diff --git a/AetherBags/Nodes/Configuration/Category/StateFilterRowNode.cs b/AetherBags/Nodes/Configuration/Category/StateFilterRowNode.cs index d11d251..707a708 100644 --- a/AetherBags/Nodes/Configuration/Category/StateFilterRowNode.cs +++ b/AetherBags/Nodes/Configuration/Category/StateFilterRowNode.cs @@ -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 + { + private static readonly string[] StateLabels = ["Ignored", "Required", "Excluded"]; + + protected override string GetStateText(int state) + => state >= 0 && state < StateLabels.Length ?StateLabels[state] : "Unknown"; } } \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/StringListEditorNode.cs b/AetherBags/Nodes/Configuration/Category/StringListEditorNode.cs index cc13f98..5cbcd65 100644 --- a/AetherBags/Nodes/Configuration/Category/StringListEditorNode.cs +++ b/AetherBags/Nodes/Configuration/Category/StringListEditorNode.cs @@ -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 _list; - private readonly TextInputNode _addInput; + private const float LabelWidth = 300f; + private const float RowHeight = 28f; + + private List _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 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 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) - { - Size = new Vector2(280, 22), - OnRemove = () => - { - _list.Remove(value); - RefreshItems(); - _onChanged?.Invoke(); - }, - } - ); + _itemsContainer.Clear(); + + foreach (var value in _list) + { + _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); + }); } } \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/UintListEditorNode.cs b/AetherBags/Nodes/Configuration/Category/UintListEditorNode.cs index 93dbef1..458ab75 100644 --- a/AetherBags/Nodes/Configuration/Category/UintListEditorNode.cs +++ b/AetherBags/Nodes/Configuration/Category/UintListEditorNode.cs @@ -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 _list; - private readonly NumericInputNode _addInput; + private const float LabelWidth = 300f; + private const float RowHeight = 28f; + + private List _list = []; + + private readonly LabelTextNode _headerLabel; private readonly VerticalListNode _itemsContainer; - private readonly Action? _onChanged; - private readonly Func? _labelResolver; + private readonly HorizontalListNode _addRow; + private readonly NumericInputNode _addInput; - public UintListEditorNode(string label, List list, Action? onChanged = null, Func? labelResolver = null) + public Func? 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 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) - { - Size = new Vector2(280, 22), - OnRemove = () => - { - _list.Remove(value); - RefreshItems(); - _onChanged?.Invoke(); - }, - } - ); + _itemsContainer.Clear(); + + foreach (var value in _list) + { + _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 +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); + }); } } \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/General/ImportExportResetNode.cs b/AetherBags/Nodes/Configuration/General/ImportExportResetNode.cs index 24ab533..a8a82fb 100644 --- a/AetherBags/Nodes/Configuration/General/ImportExportResetNode.cs +++ b/AetherBags/Nodes/Configuration/General/ImportExportResetNode.cs @@ -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 });