From f54ac8b0e578f19f73753895ef9a146293092654 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 Feb 2026 17:08:25 +0100 Subject: [PATCH] Finalize unlockstable, update design combos, add events for favorites, minor fixes. --- Glamourer/DesignPanelFlag.cs | 9 +- .../Designs/Special/QuickSelectedDesign.cs | 16 +- Glamourer/Gui/DesignCombo.cs | 646 +++++++------- Glamourer/Gui/DesignQuickBar.cs | 4 +- Glamourer/Gui/Equipment/BaseItemCombo.cs | 13 +- .../AutomationTab/RandomRestrictionDrawer.cs | 8 +- .../Gui/Tabs/DesignTab/DesignLinkDrawer.cs | 8 +- .../Gui/Tabs/UnlocksTab/UnlockCacheItem.cs | 51 ++ Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs | 841 +++++++++--------- Glamourer/Gui/Tabs/UnlocksTab/UnlocksTab.cs | 9 +- Glamourer/Gui/UiHelpers.cs | 3 +- Glamourer/Services/DesignResolver.cs | 4 +- Glamourer/Unlocks/FavoriteManager.cs | 38 +- Luna | 2 +- Penumbra.GameData | 2 +- 15 files changed, 817 insertions(+), 837 deletions(-) create mode 100644 Glamourer/Gui/Tabs/UnlocksTab/UnlockCacheItem.cs diff --git a/Glamourer/DesignPanelFlag.cs b/Glamourer/DesignPanelFlag.cs index 1b049d2..2416866 100644 --- a/Glamourer/DesignPanelFlag.cs +++ b/Glamourer/DesignPanelFlag.cs @@ -1,6 +1,5 @@ using Luna.Generators; using ImSharp; -using Luna; namespace Glamourer; @@ -41,8 +40,8 @@ public enum DesignPanelFlag : uint public static partial class DesignPanelFlagExtensions { - private static readonly SizedString Expand = new("Expand"u8); - private static readonly SizedString AdvancedCustomization = new(DesignPanelFlag.AdvancedCustomizations.ToNameU8()); + private static readonly StringU8 Expand = new("Expand"u8); + private static readonly StringU8 AdvancedCustomization = DesignPanelFlag.AdvancedCustomizations.ToNameU8(); public static Im.HeaderDisposable Header(this DesignPanelFlag flag, Configuration config) { @@ -56,8 +55,8 @@ public static partial class DesignPanelFlagExtensions public static void DrawTable(ReadOnlySpan label, DesignPanelFlag hidden, DesignPanelFlag expanded, Action setterHide, Action setterExpand) { - var checkBoxWidth = Math.Max(Im.Style.FrameHeight, Expand.Size.X); - var textWidth = AdvancedCustomization.Size.X; + var checkBoxWidth = Math.Max(Im.Style.FrameHeight, Expand.CalculateSize().X); + var textWidth = AdvancedCustomization.CalculateSize().X; var tableSize = 2 * (textWidth + 2 * checkBoxWidth) + 10 * Im.Style.CellPadding.X + 2 * Im.Style.WindowPadding.X diff --git a/Glamourer/Designs/Special/QuickSelectedDesign.cs b/Glamourer/Designs/Special/QuickSelectedDesign.cs index b4b9832..a27f5e4 100644 --- a/Glamourer/Designs/Special/QuickSelectedDesign.cs +++ b/Glamourer/Designs/Special/QuickSelectedDesign.cs @@ -20,18 +20,18 @@ public class QuickSelectedDesign(QuickDesignCombo combo) : IDesignStandIn, IServ => ResolvedName; public Design? CurrentDesign - => combo.Design as Design; + => combo.QuickDesign as Design; public ref readonly DesignData GetDesignData(in DesignData baseRef) { - if (combo.Design != null) - return ref combo.Design.GetDesignData(baseRef); + if (combo.QuickDesign is not null) + return ref combo.QuickDesign.GetDesignData(baseRef); return ref baseRef; } public IReadOnlyList<(uint, MaterialValueDesign)> GetMaterialData() - => combo.Design?.GetMaterialData() ?? []; + => combo.QuickDesign?.GetMaterialData() ?? []; public string SerializeName() => SerializedName; @@ -40,7 +40,7 @@ public class QuickSelectedDesign(QuickDesignCombo combo) : IDesignStandIn, IServ => StateSource.Manual; public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication) - => combo.Design?.AllLinks(newApplication) ?? []; + => combo.QuickDesign?.AllLinks(newApplication) ?? []; public void AddData(JObject jObj) { } @@ -52,11 +52,11 @@ public class QuickSelectedDesign(QuickDesignCombo combo) : IDesignStandIn, IServ => false; public bool ForcedRedraw - => combo.Design?.ForcedRedraw ?? false; + => combo.QuickDesign?.ForcedRedraw ?? false; public bool ResetAdvancedDyes - => combo.Design?.ResetAdvancedDyes ?? false; + => combo.QuickDesign?.ResetAdvancedDyes ?? false; public bool ResetTemporarySettings - => combo.Design?.ResetTemporarySettings ?? false; + => combo.QuickDesign?.ResetTemporarySettings ?? false; } diff --git a/Glamourer/Gui/DesignCombo.cs b/Glamourer/Gui/DesignCombo.cs index 0277f0e..36cc5fa 100644 --- a/Glamourer/Gui/DesignCombo.cs +++ b/Glamourer/Gui/DesignCombo.cs @@ -1,427 +1,383 @@ -using Dalamud.Interface.Utility.Raii; -using Glamourer.Automation; +using Glamourer.Automation; using Glamourer.Designs; using Glamourer.Designs.History; using Glamourer.Designs.Special; using Glamourer.Events; -using Dalamud.Bindings.ImGui; using ImSharp; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Extensions; -using OtterGui.Log; -using OtterGui.Widgets; -using MouseWheelType = OtterGui.Widgets.MouseWheelType; +using Luna; namespace Glamourer.Gui; -//public abstract class DesignComboBase2 : ImSharp.FilterComboBase, IDisposable -//{ -// protected readonly EphemeralConfig Config; -// protected readonly DesignChanged DesignChanged; -// protected readonly DesignColors DesignColors; -// protected readonly TabSelected TabSelected; -// protected IDesignStandIn? _currentDesign; -// -// private CacheItem CreateItem(IDesignStandIn design) -// { -// -// } -// -// public readonly struct CacheItem(IDesignStandIn design, Vector4 color) -// { -// public readonly IDesignStandIn Design = design; -// public readonly StringPair Name = new(design.ResolveName(false)); -// public readonly StringPair Incognito = new(design.ResolveName(true)); -// public readonly StringPair FullPath = StringPair.Empty; -// public readonly Vector4 Color = color; -// } -// -// public DesignComboBase2(EphemeralConfig config, DesignChanged designChanged, DesignColors designColors, TabSelected tabSelected) -// { -// Config = config; -// DesignChanged = designChanged; -// DesignColors = designColors; -// TabSelected = tabSelected; -// -// DesignChanged.Subscribe(OnDesignChanged, DesignChanged.Priority.DesignCombo); -// } -// -// private void OnDesignChanged(DesignChanged.Type type, Design? _1, ITransaction? _2 = null) -// { -// _isCurrentSelectionDirty = type switch -// { -// DesignChanged.Type.Created => true, -// DesignChanged.Type.Renamed => true, -// DesignChanged.Type.ChangedColor => true, -// DesignChanged.Type.Deleted => true, -// DesignChanged.Type.QuickDesignBar => true, -// _ => _isCurrentSelectionDirty, -// }; -// } -// -// protected override bool DrawItem(in CacheItem item, int globalIndex, bool selected) -// { -// -// } -// -// public void Dispose() -// { -// DesignChanged.Unsubscribe(OnDesignChanged); -// } -//} - -public abstract class DesignComboBase : FilterComboCache>, IDisposable +public abstract class DesignComboBase( + EphemeralConfig config, + DesignManager designs, + DesignChanged designChanged, + DesignColors designColors, + TabSelected tabSelected, + DesignFileSystem designFileSystem) + : FilterComboBase(new DesignFilter(), ConfigData.Default with { ComputeWidth = true }) { - protected readonly EphemeralConfig Config; - protected readonly DesignChanged DesignChanged; - protected readonly DesignColors DesignColors; - protected readonly TabSelected TabSelected; - protected float InnerWidth; - private IDesignStandIn? _currentDesign; - private bool _isCurrentSelectionDirty; + protected readonly EphemeralConfig Config = config; + protected readonly DesignChanged DesignChanged = designChanged; + protected readonly DesignColors DesignColors = designColors; + protected readonly DesignFileSystem DesignFileSystem = designFileSystem; + protected readonly TabSelected TabSelected = tabSelected; + protected readonly DesignManager Designs = designs; + protected IDesignStandIn? CurrentDesign; - protected DesignComboBase(Func>> generator, Logger log, DesignChanged designChanged, - TabSelected tabSelected, EphemeralConfig config, DesignColors designColors) - : base(generator, MouseWheelType.Control, log) + protected CacheItem CreateItem(IDesignStandIn design) { - DesignChanged = designChanged; - TabSelected = tabSelected; - Config = config; - DesignColors = designColors; - DesignChanged.Subscribe(OnDesignChanged, DesignChanged.Priority.DesignCombo); + var color = design is Design d1 ? DesignColors.GetColor(d1).ToVector() : ColorId.NormalDesign.Value().ToVector(); + var path = design is Design d2 && DesignFileSystem.TryGetValue(d2, out var leaf) ? leaf.FullName() : string.Empty; + var name = design.ResolveName(false); + if (path == name) + path = string.Empty; + return new CacheItem(design, color, path, name); } - void IDisposable.Dispose() - { - DesignChanged.Unsubscribe(OnDesignChanged); - GC.SuppressFinalize(this); - } + protected override bool IsSelected(CacheItem item, int globalIndex) + => item.Design == CurrentDesign; - protected override bool DrawSelectable(int globalIdx, bool selected) + public virtual bool Draw(Utf8StringHandler label, IDesignStandIn? currentDesign, out IDesignStandIn? newSelection, + float width) { - var (design, path) = Items[globalIdx]; + CurrentDesign = currentDesign; bool ret; - switch (design) + using (ImGuiColor.Text.Push(DesignColors.GetColor(CurrentDesign as Design))) { - case Design realDesign: - { - using var color = ImGuiColor.Text.Push(DesignColors.GetColor(realDesign)); - ret = base.DrawSelectable(globalIdx, selected); - DrawPath(path, realDesign); - return ret; - } - case QuickSelectedDesign quickDesign: - { - using var color = ImGuiColor.Text.Push(ColorId.NormalDesign.Value()); - ret = base.DrawSelectable(globalIdx, selected); - DrawResolvedDesign(quickDesign); - return ret; - } - default: return base.DrawSelectable(globalIdx, selected); - } - } - - private static void DrawPath(string path, Design realDesign) - { - if (path.Length <= 0 || realDesign.Name == path) - return; - - DrawRightAligned(realDesign.Name, path, ImGuiColor.TextDisabled.Get().Color); - } - - private void DrawResolvedDesign(QuickSelectedDesign quickDesign) - { - var linkedDesign = quickDesign.CurrentDesign; - if (linkedDesign != null) - DrawRightAligned(quickDesign.ResolveName(false), linkedDesign.Name.Text, DesignColors.GetColor(linkedDesign)); - else - DrawRightAligned(quickDesign.ResolveName(false), "[Nothing]", DesignColors.MissingColor); - } - - protected bool Draw(IDesignStandIn? currentDesign, string? label, float width) - { - _currentDesign = currentDesign; - UpdateCurrentSelection(); - InnerWidth = 400 * Im.Style.GlobalScale; - var name = label ?? "Select Design Here..."; - bool ret; - using (currentDesign is not null ? ImGuiColor.Text.Push(DesignColors.GetColor(currentDesign as Design)) : null) - { - ret = Draw("##design", name, string.Empty, width, Im.Style.TextHeightWithSpacing) && CurrentSelection is not null; + ret = currentDesign is null + ? base.Draw(label, "Select Design Here..."u8, StringU8.Empty, width, out var result) + : base.Draw(label, currentDesign.ResolveName(Config.IncognitoMode), StringU8.Empty, width, out result); + newSelection = ret ? result.Design : currentDesign; } - if (currentDesign is Design design) + if (CurrentDesign is Design design) { if (Im.Item.RightClicked() && Im.Io.KeyControl) TabSelected.Invoke(MainTabType.Designs, design); - ImGuiUtil.HoverTooltip("Control + Right-Click to move to design."); + Im.Tooltip.OnHover("Control + Right-Click to move to design."u8); + } + else + { + QuickSelectedDesignTooltip(CurrentDesign as QuickSelectedDesign); } - QuickSelectedDesignTooltip(currentDesign); - - _currentDesign = null; + CurrentDesign = null; return ret; } - protected override string ToString(Tuple obj) - => obj.Item1.ResolveName(Config.IncognitoMode); - - protected override float GetFilterWidth() - => InnerWidth - 2 * Im.Style.FramePadding.X; - - protected override bool IsVisible(int globalIndex, LowerString filter) + private void QuickSelectedDesignTooltip(QuickSelectedDesign? design) { - var (design, path) = Items[globalIndex]; - return filter.IsContained(path) || filter.IsContained(design.ResolveName(false)); - } - - protected override void OnMouseWheel(string preview, ref int _2, int steps) - { - if (!ReferenceEquals(_currentDesign, CurrentSelection?.Item1)) - CurrentSelectionIdx = -1; - - base.OnMouseWheel(preview, ref _2, steps); - } - - private void UpdateCurrentSelection() - { - if (!_isCurrentSelectionDirty) + if (design is null) return; - var priorState = IsInitialized; - if (priorState) - Cleanup(); - CurrentSelectionIdx = Items.IndexOf(s => ReferenceEquals(s.Item1, CurrentSelection?.Item1)); - if (CurrentSelectionIdx >= 0) - { - UpdateSelection(Items[CurrentSelectionIdx]); - } - else if (Items.Count > 0) - { - CurrentSelectionIdx = 0; - UpdateSelection(Items[0]); - } - else - { - UpdateSelection(null); - } - - if (!priorState) - Cleanup(); - _isCurrentSelectionDirty = false; - } - - protected override int UpdateCurrentSelected(int currentSelected) - { - CurrentSelectionIdx = Items.IndexOf(p => _currentDesign == p.Item1); - UpdateSelection(CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : null); - return CurrentSelectionIdx; - } - - private void OnDesignChanged(DesignChanged.Type type, Design? _1, ITransaction? _2 = null) - { - _isCurrentSelectionDirty = type switch - { - DesignChanged.Type.Created => true, - DesignChanged.Type.Renamed => true, - DesignChanged.Type.ChangedColor => true, - DesignChanged.Type.Deleted => true, - DesignChanged.Type.QuickDesignBar => true, - _ => _isCurrentSelectionDirty, - }; - } - - private void QuickSelectedDesignTooltip(IDesignStandIn? design) - { - if (!ImGui.IsItemHovered()) + if (!Im.Item.Hovered()) return; - if (design is not QuickSelectedDesign q) - return; - - using var tt = ImRaii.Tooltip(); - var linkedDesign = q.CurrentDesign; - if (linkedDesign != null) + using var tt = Im.Tooltip.Begin(); + var linkedDesign = design.CurrentDesign; + if (linkedDesign is not null) { - ImGui.TextUnformatted("Currently resolving to "); + Im.Text("Currently resolving to "u8); using var color = ImGuiColor.Text.Push(DesignColors.GetColor(linkedDesign)); - ImGui.SameLine(0, 0); - ImGui.TextUnformatted(linkedDesign.Name.Text); + Im.Line.NoSpacing(); + Im.Text(linkedDesign.Name.Text); } else { - ImGui.TextUnformatted("No design selected in the Quick Design Bar."); + Im.Text("No design selected in the Quick Design Bar."u8); } } - private static void DrawRightAligned(string leftText, string text, Rgba32 color) + protected sealed class DesignFilter : Utf8FilterBase { - var start = ImGui.GetItemRectMin(); - var pos = start.X + ImGui.CalcTextSize(leftText).X; - var maxSize = ImGui.GetWindowPos().X + ImGui.GetWindowContentRegionMax().X; - var remainingSpace = maxSize - pos; - var requiredSize = ImGui.CalcTextSize(text).X + Im.Style.ItemInnerSpacing.X; - var offset = remainingSpace - requiredSize; - if (ImGui.GetScrollMaxY() == 0) - offset -= Im.Style.ItemInnerSpacing.X; - - if (offset < Im.Style.ItemSpacing.X) - ImGuiUtil.HoverTooltip(text); - else - Im.Window.DrawList.Text(start with { X = pos + offset }, color, text); - } -} - -public abstract class DesignCombo : DesignComboBase -{ - protected DesignCombo(Logger log, DesignChanged designChanged, TabSelected tabSelected, - EphemeralConfig config, DesignColors designColors, Func>> generator) - : base(generator, log, designChanged, tabSelected, config, designColors) - { - if (Items.Count == 0) - return; - - CurrentSelection = Items[0]; - CurrentSelectionIdx = 0; - base.Cleanup(); - } - - public IDesignStandIn? Design - => CurrentSelection?.Item1; - - public void Draw(float width) - => Draw(Design, Design?.ResolveName(Config.IncognitoMode) ?? string.Empty, width); -} - -public sealed class QuickDesignCombo : DesignCombo -{ - public QuickDesignCombo(DesignFileSystem fileSystem, - Logger log, - DesignChanged designChanged, - TabSelected tabSelected, - EphemeralConfig config, - DesignColors designColors) - : base(log, designChanged, tabSelected, config, designColors, () => - [ - .. fileSystem - .Where(kvp => kvp.Key.QuickDesign) - .Select(kvp => new Tuple(kvp.Key, kvp.Value.FullName())) - .OrderBy(d => d.Item2), - ]) - { - if (config.SelectedQuickDesign != Guid.Empty) + public override bool DrawFilter(ReadOnlySpan label, Vector2 availableRegion) { - CurrentSelectionIdx = Items.IndexOf(t => t.Item1 is Design d && d.Identifier == config.SelectedQuickDesign); - if (CurrentSelectionIdx >= 0) - CurrentSelection = Items[CurrentSelectionIdx]; - else if (Items.Count > 0) - CurrentSelectionIdx = 0; + using var _ = ImGuiColor.Text.PushDefault(); + return base.DrawFilter(label, availableRegion); } - AllowMouseWheel = MouseWheelType.Unmodified; - SelectionChanged += OnSelectionChange; + public override bool WouldBeVisible(in CacheItem item, int globalIndex) + => WouldBeVisible(item.Name.Utf8) || WouldBeVisible(item.Incognito.Utf8) || WouldBeVisible(item.FullPath.Utf8); + + protected override ReadOnlySpan ToFilterString(in CacheItem item, int globalIndex) + => item.Name.Utf8; } - private void OnSelectionChange(Tuple? old, Tuple? @new) + protected sealed class Cache : FilterComboBaseCache { - if (old == null) + private new DesignComboBase Parent + => (DesignComboBase)base.Parent; + + public Cache(DesignComboBase parent) + : base(parent) { - if (@new?.Item1 is not Design d) + Parent.DesignColors.ColorChanged += OnDesignColorChanged; + Parent.DesignChanged.Subscribe(OnDesignChanged, DesignChanged.Priority.DesignCombo); + } + + protected override void ComputeWidth() + => ComboWidth = UnfilteredItems.Max(d + => d.Name.Utf8.CalculateSize(false).X + d.FullPath.Utf8.CalculateSize(false).X + 2 * Im.Style.ItemSpacing.X + Im.Style.ScrollbarSize); + + protected override void Dispose(bool disposing) + { + Parent.DesignColors.ColorChanged -= OnDesignColorChanged; + Parent.DesignChanged.Unsubscribe(OnDesignChanged); + base.Dispose(disposing); + } + + private void OnDesignColorChanged() + => Dirty |= IManagedCache.DirtyFlags.Custom; + + private void OnDesignChanged(DesignChanged.Type type, Design? _1, ITransaction? _2 = null) + { + if (type switch + { + DesignChanged.Type.Created => true, + DesignChanged.Type.Renamed => true, + DesignChanged.Type.ChangedColor => true, + DesignChanged.Type.Deleted => true, + DesignChanged.Type.QuickDesignBar => true, + _ => false, + }) + Dirty |= IManagedCache.DirtyFlags.Custom; + } + } + + protected override FilterComboBaseCache CreateCache() + => new Cache(this); + + public readonly struct CacheItem(IDesignStandIn design, Vector4 color, string path, string name) + { + public readonly IDesignStandIn Design = design; + public readonly StringPair Name = new(name); + public readonly StringPair Incognito = new(design.ResolveName(true)); + public readonly StringPair FullPath = new(path); + public readonly Vector4 Color = color; + + public static string Ordering(CacheItem item) + => item.FullPath.Utf16.Length > 0 ? item.FullPath.Utf16 : item.Name.Utf16; + } + + + protected override bool DrawItem(in CacheItem item, int globalIndex, bool selected) + { + using var color = ImGuiColor.Text.Push(item.Color); + var name = Config.IncognitoMode ? item.Incognito.Utf8 : item.Name.Utf8; + var ret = Im.Selectable(name, selected); + if (!item.FullPath.IsEmpty && !Config.IncognitoMode) + { + Im.Line.Same(); + color.Push(ImGuiColor.Text, Im.Style[ImGuiColor.TextDisabled]); + ImEx.TextRightAligned(item.FullPath.Utf8); + } + else if (item.Design is QuickSelectedDesign { CurrentDesign: { } d }) + { + Im.Line.Same(); + color.Push(ImGuiColor.Text, DesignColors.GetColor(d)); + ImEx.TextRightAligned(d.ResolveName(Config.IncognitoMode)); + } + + return ret; + } + + protected override float ItemHeight + => Im.Style.TextHeightWithSpacing; +} + +public sealed class QuickDesignCombo : DesignComboBase, IDisposable, IUiService +{ + public Design? QuickDesign + { + get; + private set + { + if (field == value) return; - Config.SelectedQuickDesign = d.Identifier; - Config.Save(); - } - else if (@new?.Item1 is not Design d) - { - Config.SelectedQuickDesign = Guid.Empty; - Config.Save(); - } - else if (!old.Item1.Equals(@new.Item1)) - { - Config.SelectedQuickDesign = d.Identifier; + field = value; + Config.SelectedQuickDesign = field?.Identifier ?? Guid.Empty; Config.Save(); } } + + + public QuickDesignCombo(EphemeralConfig config, DesignChanged designChanged, DesignColors designColors, TabSelected tabSelected, + DesignFileSystem designFileSystem, DesignManager designs) + : base(config, designs, designChanged, designColors, tabSelected, designFileSystem) + { + if (Designs.Designs.TryGetValue(config.SelectedQuickDesign, out var design) && design.QuickDesign) + QuickDesign = design; + DesignChanged.Subscribe(OnDesignChanged, DesignChanged.Priority.DesignCombo); + } + + private void OnDesignChanged(DesignChanged.Type type, Design changedDesign, ITransaction? _) + { + switch (type) + { + case DesignChanged.Type.Created: + // If the quick design bar has no selection, select the new design if it supports the bar. + if (QuickDesign is null && changedDesign.QuickDesign) + QuickDesign = changedDesign; + break; + case DesignChanged.Type.Deleted: + // If the deleted design was selected, select the first design that supports the bar, if any. + if (QuickDesign == changedDesign) + QuickDesign = Designs.Designs.FirstOrDefault(d => d.QuickDesign); + break; + case DesignChanged.Type.ReloadedAll: + // If all designs were reloaded, update the selection. + QuickDesign = Designs.Designs.TryGetValue(Config.SelectedQuickDesign, out var design) && design.QuickDesign ? design : null; + break; + case DesignChanged.Type.QuickDesignBar: + // If the quick design support of a design was changed, select the new design if the bar has no selection and the design now supports it, + if (QuickDesign is null && changedDesign.QuickDesign) + QuickDesign = changedDesign; + // or select the first design that supports the bar, if any, if the support was removed from the currently selected design. + else if (QuickDesign == changedDesign && !changedDesign.QuickDesign) + QuickDesign = Designs.Designs.FirstOrDefault(d => d.QuickDesign); + break; + } + } + + public bool Draw(Utf8StringHandler label, float width) + { + if (!base.Draw(label, QuickDesign, out var newDesign, width)) + return false; + + QuickDesign = newDesign as Design; + return true; + } + + protected override IEnumerable GetItems() + => Designs.Designs + .Where(design => design.QuickDesign) + .Select(CreateItem) + .OrderBy(CacheItem.Ordering); + + public void Dispose() + => DesignChanged.Unsubscribe(OnDesignChanged); } -public sealed class LinkDesignCombo( - DesignFileSystem fileSystem, - Logger log, - DesignChanged designChanged, - TabSelected tabSelected, - EphemeralConfig config, - DesignColors designColors) - : DesignCombo(log, designChanged, tabSelected, config, designColors, () => - [ - .. fileSystem - .Select(kvp => new Tuple(kvp.Key, kvp.Value.FullName())) - .OrderBy(d => d.Item2), - ]); +public sealed class LinkDesignCombo : DesignComboBase, IUiService, IDisposable +{ + public Design? NewSelection { get; private set; } + + public LinkDesignCombo(EphemeralConfig config, DesignChanged designChanged, DesignColors designColors, TabSelected tabSelected, + DesignFileSystem designFileSystem, DesignManager designs) + : base(config, designs, designChanged, designColors, tabSelected, designFileSystem) + { + DesignChanged.Subscribe(OnDesignChanged, DesignChanged.Priority.DesignCombo); + } + + public bool Draw(Utf8StringHandler label, float width) + { + if (!base.Draw(label, NewSelection, out var newSelection, width)) + return false; + + NewSelection = newSelection as Design; + return true; + } + + protected override IEnumerable GetItems() + => Designs.Designs.Select(CreateItem) + .OrderBy(CacheItem.Ordering); + + public void Dispose() + => DesignChanged.Unsubscribe(OnDesignChanged); + + private void OnDesignChanged(DesignChanged.Type type, Design design, ITransaction? _) + { + if (type is DesignChanged.Type.Deleted && design == NewSelection || type is DesignChanged.Type.ReloadedAll) + NewSelection = null; + } +} public sealed class RandomDesignCombo( - DesignManager designs, - DesignFileSystem fileSystem, - Logger log, - DesignChanged designChanged, - TabSelected tabSelected, EphemeralConfig config, - DesignColors designColors) - : DesignCombo(log, designChanged, tabSelected, config, designColors, () => - [ - .. fileSystem - .Select(kvp => new Tuple(kvp.Key, kvp.Value.FullName())) - .OrderBy(d => d.Item2), - ]) + DesignManager designs, + DesignChanged designChanged, + DesignColors designColors, + TabSelected tabSelected, + DesignFileSystem designFileSystem) : DesignComboBase(config, designs, designChanged, designColors, tabSelected, designFileSystem), + IUiService { private Design? GetDesign(RandomPredicate.Exact exact) { return exact.Which switch { - RandomPredicate.Exact.Type.Name => designs.Designs.FirstOrDefault(d => d.Name == exact.Value), - RandomPredicate.Exact.Type.Path => fileSystem.Find(exact.Value.Text, out var c) && c is DesignFileSystem.Leaf l ? l.Value : null, - RandomPredicate.Exact.Type.Identifier => designs.Designs.ByIdentifier(Guid.TryParse(exact.Value.Text, out var g) ? g : Guid.Empty), + RandomPredicate.Exact.Type.Name => Designs.Designs.FirstOrDefault(d => d.Name == exact.Value), + RandomPredicate.Exact.Type.Path => DesignFileSystem.Find(exact.Value.Text, out var c) && c is DesignFileSystem.Leaf l + ? l.Value + : null, + RandomPredicate.Exact.Type.Identifier => Designs.Designs.ByIdentifier(Guid.TryParse(exact.Value.Text, out var g) + ? g + : Guid.Empty), _ => null, }; } - public bool Draw(RandomPredicate.Exact exact, float width) + public bool Draw(RandomPredicate.Exact exact, [NotNullWhen(true)] out Design? newDesign, float width) { var design = GetDesign(exact); - return Draw(design, design?.ResolveName(Config.IncognitoMode) ?? $"Not Found [{exact.Value.Text}]", width); + if (Draw(StringU8.Empty, design?.ResolveName(Config.IncognitoMode) ?? $"Not Found [{exact.Value.Text}]", StringU8.Empty, width, + out var newItem) + && newItem.Design is Design d) + { + newDesign = d; + return true; + } + + newDesign = null; + return false; } - public bool Draw(IDesignStandIn? design, float width) - => Draw(design, design?.ResolveName(Config.IncognitoMode) ?? string.Empty, width); + protected override IEnumerable GetItems() + => Designs.Designs.Select(CreateItem) + .OrderBy(CacheItem.Ordering); } -public sealed class SpecialDesignCombo( - DesignFileSystem fileSystem, - TabSelected tabSelected, - DesignColors designColors, - Logger log, - DesignChanged designChanged, - AutoDesignManager autoDesignManager, - EphemeralConfig config, - RandomDesignGenerator rng, - QuickSelectedDesign quickSelectedDesign) - : DesignComboBase(() => fileSystem - .Select(kvp => new Tuple(kvp.Key, kvp.Value.FullName())) - .OrderBy(d => d.Item2) - .Prepend(new Tuple(new RandomDesign(rng), string.Empty)) - .Prepend(new Tuple(quickSelectedDesign, string.Empty)) - .Prepend(new Tuple(new RevertDesign(), string.Empty)) - .ToList(), log, designChanged, tabSelected, config, designColors) +public sealed class SpecialDesignCombo : DesignComboBase, IUiService { + private readonly AutoDesignManager _autoDesigns; + + private readonly CacheItem _random; + private readonly CacheItem _revert; + private readonly CacheItem _quick; + + public SpecialDesignCombo(EphemeralConfig config, + DesignManager designs, + DesignChanged designChanged, + DesignColors designColors, + TabSelected tabSelected, + DesignFileSystem designFileSystem, + AutoDesignManager autoDesigns, + RandomDesignGenerator rng, QuickSelectedDesign quickSelectedDesign) + : base(config, designs, designChanged, designColors, tabSelected, designFileSystem) + { + _autoDesigns = autoDesigns; + _random = CreateItem(new RandomDesign(rng)); + _revert = CreateItem(new RevertDesign()); + _quick = CreateItem(quickSelectedDesign); + } + public void Draw(AutoDesignSet set, AutoDesign? design, int autoDesignIndex) { - if (!Draw(design?.Design, design?.Design.ResolveName(Config.IncognitoMode), Im.ContentRegion.Available.X)) + if (!Draw(StringU8.Empty, design?.Design, out var newSelection, Im.ContentRegion.Available.X) || newSelection is null) return; if (autoDesignIndex >= 0) - autoDesignManager.ChangeDesign(set, autoDesignIndex, CurrentSelection!.Item1); + _autoDesigns.ChangeDesign(set, autoDesignIndex, newSelection); else - autoDesignManager.AddDesign(set, CurrentSelection!.Item1); + _autoDesigns.AddDesign(set, newSelection); } + + protected override IEnumerable GetItems() + => Designs.Designs + .Select(CreateItem) + .OrderBy(CacheItem.Ordering) + .Prepend(_random) + .Prepend(_quick) + .Prepend(_revert); } diff --git a/Glamourer/Gui/DesignQuickBar.cs b/Glamourer/Gui/DesignQuickBar.cs index afdb81b..9e9fa80 100644 --- a/Glamourer/Gui/DesignQuickBar.cs +++ b/Glamourer/Gui/DesignQuickBar.cs @@ -109,7 +109,7 @@ public sealed class DesignQuickBar : Window, IDisposable if (_config.QdbButtons.HasFlag(QdbButtons.ApplyDesign)) { var comboSize = width - _numButtons * (buttonSize.X + spacing.X); - _designCombo.Draw(comboSize); + _designCombo.Draw(StringU8.Empty, comboSize); Im.Line.Same(); DrawApplyButton(buttonSize); } @@ -142,7 +142,7 @@ public sealed class DesignQuickBar : Window, IDisposable private void DrawApplyButton(Vector2 size) { - var design = _designCombo.Design as Design; + var design = _designCombo.QuickDesign; var available = 0; _tooltipBuilder.Clear(); diff --git a/Glamourer/Gui/Equipment/BaseItemCombo.cs b/Glamourer/Gui/Equipment/BaseItemCombo.cs index d7b5fdb..8b39a5d 100644 --- a/Glamourer/Gui/Equipment/BaseItemCombo.cs +++ b/Glamourer/Gui/Equipment/BaseItemCombo.cs @@ -34,14 +34,11 @@ public abstract class BaseItemCombo(FavoriteManager favorites, ItemManager items return false; } - public readonly struct CacheItem(EquipItem item) : IDisposable + public readonly struct CacheItem(EquipItem item) { - public readonly EquipItem Item = item; - public readonly StringPair Name = new(item.Name); - public readonly SizedStringPair Model = new($"({item.PrimaryId.Id}-{item.Variant.Id})"); - - public void Dispose() - => Model.Dispose(); + public readonly EquipItem Item = item; + public readonly StringPair Name = new(item.Name); + public readonly StringPair Model = new($"({item.PrimaryId.Id}-{item.Variant.Id})"); } protected sealed class ItemFilter : PartwiseFilterBase @@ -85,7 +82,7 @@ public abstract class BaseItemCombo(FavoriteManager favorites, ItemManager items var ret = Im.Selectable(item.Name.Utf8, selected); Im.Line.Same(); using var color = ImGuiColor.Text.Push(Rgba32.Gray); - ImEx.TextRightAligned(item.Model); + ImEx.TextRightAligned(item.Model.Utf8); return ret; } diff --git a/Glamourer/Gui/Tabs/AutomationTab/RandomRestrictionDrawer.cs b/Glamourer/Gui/Tabs/AutomationTab/RandomRestrictionDrawer.cs index ca68776..d11667e 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/RandomRestrictionDrawer.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/RandomRestrictionDrawer.cs @@ -236,9 +236,9 @@ public sealed class RandomRestrictionDrawer : IService, IDisposable { ImEx.TextFrameAligned("that are exactly"u8); table.NextColumn(); - if (_randomDesignCombo.Draw(exact, Im.ContentRegion.Available.X) && _randomDesignCombo.Design is Design d) + if (_randomDesignCombo.Draw(exact, out var newDesign, Im.ContentRegion.Available.X)) { - list[i] = new RandomPredicate.Exact(RandomPredicate.Exact.Type.Identifier, d.Identifier.ToString()); + list[i] = new RandomPredicate.Exact(RandomPredicate.Exact.Type.Identifier, newDesign.Identifier.ToString()); _autoDesignManager.ChangeData(_set!, _designIndex, list); } @@ -314,8 +314,8 @@ public sealed class RandomRestrictionDrawer : IService, IDisposable "Add a new condition that the design must be assigned to the given color."u8, invalid) && Add(new RandomPredicate.Exact(RandomPredicate.Exact.Type.Color, _newText)); - if (_randomDesignCombo.Draw(_newDesign, Im.ContentRegion.Available.X - Im.Style.ItemInnerSpacing.X - buttonSize.X)) - _newDesign = _randomDesignCombo.CurrentSelection?.Item1 as Design; + if (_randomDesignCombo.Draw(StringU8.Empty, _newDesign, out var newDesign, Im.ContentRegion.Available.X - Im.Style.ItemInnerSpacing.X - buttonSize.X)) + _newDesign = newDesign as Design; Im.Line.SameInner(); if (ImEx.Button("Exact Design"u8, buttonSize, "Add a single, specific design."u8, _newDesign is null)) { diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignLinkDrawer.cs b/Glamourer/Gui/Tabs/DesignTab/DesignLinkDrawer.cs index 4c22df1..7cfc964 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignLinkDrawer.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignLinkDrawer.cs @@ -151,12 +151,12 @@ public class DesignLinkDrawer( { table.NextColumn(); table.NextColumn(); - combo.Draw(Im.ContentRegion.Available.X); + combo.Draw(StringU8.Empty, Im.ContentRegion.Available.X); table.NextColumn(); string ttBefore, ttAfter; bool canAddBefore, canAddAfter; - var design = combo.Design as Design; - if (design == null) + var design = combo.NewSelection; + if (design is null) { ttAfter = ttBefore = "Select a design first."; canAddBefore = canAddAfter = false; @@ -180,7 +180,7 @@ public class DesignLinkDrawer( } Im.Line.Same(); - if (ImEx.Icon.Button(FontAwesomeIcon.ArrowCircleUp.Icon(), ttAfter, !canAddAfter)) + if (ImEx.Icon.Button(FontAwesomeIcon.ArrowCircleDown.Icon(), ttAfter, !canAddAfter)) linkManager.AddDesignLink(selector.Selected!, design!, LinkOrder.After); } diff --git a/Glamourer/Gui/Tabs/UnlocksTab/UnlockCacheItem.cs b/Glamourer/Gui/Tabs/UnlocksTab/UnlockCacheItem.cs new file mode 100644 index 0000000..e59e8d0 --- /dev/null +++ b/Glamourer/Gui/Tabs/UnlocksTab/UnlockCacheItem.cs @@ -0,0 +1,51 @@ +using ImSharp; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Tabs.UnlocksTab; + +public readonly struct UnlockCacheItem(in EquipItem item, in EquipItem offhand, in EquipItem gauntlets, in JobGroup jobs) +{ + private static readonly StringU8 Always = new("Always"u8); + + [Flags] + public enum Dyability : byte + { + No = 1, + Yes = 2, + Two = 4, + } + + public readonly EquipItem Item = item; + public readonly StringPair Name = new(item.Name); + public readonly EquipFlag Slot = item.Type.ToSlot().ToFlag(); + + public required DateTimeOffset UnlockTimestamp + { + get; + init + { + field = value; + UnlockText = value == DateTimeOffset.MinValue ? Always : + value == DateTimeOffset.MaxValue ? StringU8.Empty : new StringU8($"{value.LocalDateTime:g}"); + } + } + + public readonly StringU8 UnlockText; + + public readonly StringPair ItemId = new($"{item.ItemId.Id}"); + public readonly StringPair ModelString = new(item.ModelString); + public readonly StringPair OffhandModelString = offhand.Valid ? new StringPair(offhand.ModelString) : StringPair.Empty; + public readonly StringPair GauntletModelString = gauntlets.Valid ? new StringPair(gauntlets.ModelString) : StringPair.Empty; + public readonly StringPair RequiredLevel = new($"{item.Level.Value}"); + public required (string, string)[] Mods { get; init; } + public readonly JobFlag Jobs = jobs.Flags; + public readonly StringU8 JobText = jobs.Name.IsEmpty ? new StringU8($"Unknown {jobs.Id.Id}") : jobs.Name; + public required bool Favorite { get; init; } + public readonly bool Tradable = item.Flags.HasFlag(ItemFlags.IsTradable); + public readonly bool Crest = item.Flags.HasFlag(ItemFlags.IsCrestWorthy); + + public readonly Dyability Dyable = item.Flags.HasFlag(ItemFlags.IsDyable1) + ? item.Flags.HasFlag(ItemFlags.IsDyable2) ? Dyability.Two : Dyability.Yes + : Dyability.No; +} diff --git a/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs b/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs index acbd1d6..f6c747c 100644 --- a/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs +++ b/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs @@ -1,606 +1,561 @@ using Dalamud.Game.Text.SeStringHandling; using Dalamud.Interface; +using Dalamud.Interface.Utility.Table; using Glamourer.Events; using Glamourer.Interop; using Glamourer.Interop.Penumbra; using Glamourer.Services; using Glamourer.Unlocks; -using Dalamud.Bindings.ImGui; using ImSharp; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Table; -using OtterGui.Text; +using ImSharp.Table; +using Luna; +using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; namespace Glamourer.Gui.Tabs.UnlocksTab; -public class UnlockTable : Table, IDisposable +public sealed class UnlockTable : TableBase { - private readonly ObjectUnlocked _event; - private readonly PenumbraService _penumbra; + private readonly JobService _jobs; + private readonly ItemManager _items; + private readonly ItemUnlockManager _unlocks; + private readonly FavoriteManager _favorites; + private readonly PenumbraService _penumbra; + private readonly ObjectUnlocked _unlockEvent; - private Guid _lastCurrentCollection = Guid.Empty; - - public UnlockTable(ItemManager items, TextureService textures, ItemUnlockManager itemUnlocks, - PenumbraChangedItemTooltip tooltip, ObjectUnlocked @event, JobService jobs, FavoriteManager favorites, PenumbraService penumbra) - : base("ItemUnlockTable", new ItemList(items), - new FavoriteColumn(favorites, @event) { Label = "F" }, - new ModdedColumn(penumbra) { Label = "M" }, - new NameColumn(textures, tooltip) { Label = "Item Name..." }, - new SlotColumn { Label = "Equip Slot" }, - new TypeColumn { Label = "Item Type..." }, - new UnlockDateColumn(itemUnlocks) { Label = "Unlocked" }, - new ItemIdColumn { Label = "Item Id..." }, - new ModelDataColumn(items) { Label = "Model Data..." }, - new JobColumn(jobs) { Label = "Jobs" }, - new RequiredLevelColumn { Label = "Level..." }, - new DyableColumn { Label = "Dye" }, - new CrestColumn { Label = "Crest" }, - new TradableColumn { Label = "Trade" } - ) + public UnlockTable(JobService jobs, ItemManager items, ItemUnlockManager unlocks, PenumbraChangedItemTooltip tooltip, + ObjectUnlocked unlockEvent, FavoriteManager favorites, PenumbraService penumbra, TextureService textures) + : base(new StringU8("Unlock Table"u8), new FavoriteColumn(favorites), new ModdedColumn(), new NameColumn(textures, tooltip), + new SlotColumn(), new TypeColumn(), new UnlockDateColumn(), new ItemIdColumn(), new ModelDataColumn(), new JobColumn(jobs), + new RequiredLevelColumn(), new DyableColumn(), new CrestColumn(), new TradableColumn()) { - _event = @event; - _penumbra = penumbra; - Sortable = true; - Flags |= ImGuiTableFlags.Hideable | ImGuiTableFlags.Reorderable | ImGuiTableFlags.Resizable; - _event.Subscribe(OnObjectUnlock, ObjectUnlocked.Priority.UnlockTable); - _penumbra.ModSettingChanged += OnModSettingsChanged; - + _jobs = jobs; + _items = items; + _unlocks = unlocks; + _unlockEvent = unlockEvent; + _favorites = favorites; + _penumbra = penumbra; + + Flags |= TableFlags.Hideable | TableFlags.Reorderable | TableFlags.Resizable; } - private void OnModSettingsChanged(Penumbra.Api.Enums.ModSettingChange type, Guid collection, string mod, bool inherited) - { - if (collection != _lastCurrentCollection) - return; + public override (int Columns, int Rows) GetFrozenScroll() + => (3, 1); - FilterDirty = true; - SortDirty = true; - } + public override IEnumerable GetItems() + => _items.ItemData.AllItems(true).Select(p => ToCacheItem(p.Item2)); - protected override void PreDraw() + private UnlockCacheItem ToCacheItem(EquipItem item) { - var lastCurrentCollection = _penumbra.CurrentCollection.Id; - if (_lastCurrentCollection != lastCurrentCollection) + var unlocked = _unlocks.IsUnlocked(item.Id, out var time) ? time : DateTimeOffset.MaxValue; + EquipItem offhand = default; + EquipItem gauntlet = default; + if (item.Type.ValidOffhand().IsOffhandType()) { - _lastCurrentCollection = lastCurrentCollection; - FilterDirty = true; - SortDirty = true; + _items.ItemData.TryGetValue(item.ItemId, EquipSlot.OffHand, out offhand); + if (item.Type is FullEquipType.Fists) + { + _items.ItemData.TryGetValue(item.ItemId, EquipSlot.Hands, out gauntlet); + if (gauntlet.Type is not FullEquipType.Hands) + gauntlet = default; + } } + + var favorite = _favorites.Contains(item); + var mods = _penumbra.CheckCurrentChangedItem(item.Name); + var jobs = item.JobRestrictions.Id > 0 && item.JobRestrictions.Id < _jobs.AllJobGroups.Count + ? _jobs.AllJobGroups[item.JobRestrictions] + : _jobs.AllJobGroups[1]; + return new UnlockCacheItem(item, offhand, gauntlet, jobs) + { + UnlockTimestamp = unlocked, + Mods = mods, + Favorite = favorite, + }; } - public void Dispose() - { - _event.Unsubscribe(OnObjectUnlock); - _penumbra.ModSettingChanged -= OnModSettingsChanged; - } + protected override Cache CreateCache() + => new(this); - private sealed class FavoriteColumn : YesNoColumn + private sealed class FavoriteColumn : YesNoColumn { - public override float Width - => Im.Style.FrameHeightWithSpacing; - private readonly FavoriteManager _favorites; - private readonly ObjectUnlocked _hackEvent; // used to trigger the table dirty. - public FavoriteColumn(FavoriteManager favorites, ObjectUnlocked hackEvent) + public FavoriteColumn(FavoriteManager favorites) { - _favorites = favorites; - _hackEvent = hackEvent; - Flags |= ImGuiTableColumnFlags.NoResize; + _favorites = favorites; + Flags |= TableColumnFlags.NoResize; + Label = new StringU8("F"u8); + FilterLabel = new StringU8("Favorite"u8); } - protected override bool GetValue(EquipItem item) - => _favorites.Contains(item); - - public override void DrawColumn(EquipItem item, int idx) - { - ImGui.AlignTextToFramePadding(); - if (UiHelpers.DrawFavoriteStar(_favorites, item)) - _hackEvent.Invoke(ObjectUnlocked.Type.Customization, 0, DateTimeOffset.Now); - } - - public override bool FilterFunc(EquipItem item) - => FilterValue.HasFlag(_favorites.Contains(item) ? YesNoFlag.Yes : YesNoFlag.No); - - public override int Compare(EquipItem lhs, EquipItem rhs) - => _favorites.Contains(rhs).CompareTo(_favorites.Contains(lhs)); - } - - private sealed class ModdedColumn : YesNoColumn - { - public override float Width + public override float ComputeWidth(IEnumerable allItems) => Im.Style.FrameHeightWithSpacing; - private readonly PenumbraService _penumbra; - private readonly Dictionary _compareCache = []; - - public ModdedColumn(PenumbraService penumbra) + public override void DrawColumn(in UnlockCacheItem item, int globalIndex) { - _penumbra = penumbra; - Flags |= ImGuiTableColumnFlags.NoResize; + Im.Cursor.FrameAlign(); + UiHelpers.DrawFavoriteStar(_favorites, item.Item); } - public override void PostSort() + protected override bool GetValue(in UnlockCacheItem item, int globalIndex, int triEnumIndex) + => item.Favorite; + } + + private sealed class ModdedColumn : YesNoColumn + { + private static readonly AwesomeIcon Dot = FontAwesomeIcon.Circle; + + public ModdedColumn() { - _compareCache.Clear(); + Flags |= TableColumnFlags.NoResize; + Label = new StringU8("M"); + FilterLabel = new StringU8("Modded"u8); } - public override void DrawColumn(EquipItem item, int idx) + public override float ComputeWidth(IEnumerable allItems) + => Im.Style.FrameHeightWithSpacing; + + public override void DrawColumn(in UnlockCacheItem item, int globalIndex) { - var value = _penumbra.CheckCurrentChangedItem(item.Name); - if (value.Length == 0) + if (item.Mods.Length is 0) return; - using (ImRaii.PushFont(UiBuilder.IconFont)) + using (AwesomeIcon.Font.Push()) { using var color = ImGuiColor.Text.Push(ColorId.ModdedItemMarker.Value()); - ImGuiUtil.Center(FontAwesomeIcon.Circle.ToIconString()); + ImEx.TextCentered(Dot.Span); } - if (ImGui.IsItemHovered()) + if (Im.Item.Hovered()) { - using var tt = ImUtf8.Tooltip(); - foreach (var (_, mod) in value) - ImUtf8.BulletText(mod); + using var style = Im.Style.PushDefault(); + using var tt = Im.Tooltip.Begin(); + foreach (var (_, mod) in item.Mods) + Im.BulletText(mod); } } - public override bool FilterFunc(EquipItem item) - => FilterValue.HasFlag(_penumbra.CheckCurrentChangedItem(item.Name).Length > 0 ? YesNoFlag.Yes : YesNoFlag.No); + protected override bool GetValue(in UnlockCacheItem item, int globalIndex, int triEnumIndex) + => item.Mods.Length > 0; - public override int Compare(EquipItem lhs, EquipItem rhs) - { - if (!_compareCache.TryGetValue(lhs.Id, out var lhsCount)) - { - lhsCount = _penumbra.CheckCurrentChangedItem(lhs.Name).Length; - _compareCache[lhs.Id] = lhsCount; - } - - if (!_compareCache.TryGetValue(rhs.Id, out var rhsCount)) - { - rhsCount = _penumbra.CheckCurrentChangedItem(rhs.Name).Length; - _compareCache[rhs.Id] = rhsCount; - } - - return lhsCount.CompareTo(rhsCount); - } + public override int Compare(in UnlockCacheItem lhs, int lhsGlobalIndex, in UnlockCacheItem rhs, int rhsGlobalIndex) + => lhs.Mods.Length.CompareTo(rhs.Mods.Length); } - private sealed class NameColumn : ColumnString + private sealed class NameColumn : TextColumn { private readonly TextureService _textures; private readonly PenumbraChangedItemTooltip _tooltip; - public override float Width - => 400 * Im.Style.GlobalScale; - public NameColumn(TextureService textures, PenumbraChangedItemTooltip tooltip) { - _textures = textures; - _tooltip = tooltip; - Flags |= ImGuiTableColumnFlags.NoHide | ImGuiTableColumnFlags.NoReorder; + _textures = textures; + _tooltip = tooltip; + Flags |= TableColumnFlags.NoHide | TableColumnFlags.NoReorder; + Label = new StringU8("Item Name..."u8); + UnscaledWidth = 400; } - public override string ToName(EquipItem item) - => item.Name; - - public override void DrawColumn(EquipItem item, int _) + public override void DrawColumn(in UnlockCacheItem item, int _) { - if (_textures.TryLoadIcon(item.IconId.Id, out var iconHandle)) - ImGuiUtil.HoverIcon(iconHandle, new Vector2(Im.Style.FrameHeight)); + if (_textures.TryLoadIcon(item.Item.IconId.Id, out var iconHandle)) + Im.Image.DrawScaled(iconHandle.Id, new Vector2(Im.Style.FrameHeight), iconHandle.Size); else Im.Dummy(new Vector2(Im.Style.FrameHeight)); Im.Line.Same(); - ImGui.AlignTextToFramePadding(); - if (ImGui.Selectable(item.Name) && item.Id is { IsBonusItem: false, IsCustom: false }) - Glamourer.Messager.Chat.Print(new SeStringBuilder().AddItemLink(item.ItemId.Id, false).BuiltString); + Im.Cursor.FrameAlign(); + if (Im.Selectable(item.Name.Utf8) && item.Item.Id is { IsBonusItem: false, IsCustom: false }) + Glamourer.Messager.Chat.Print(new SeStringBuilder().AddItemLink(item.Item.ItemId.Id, false).BuiltString); if (Im.Item.RightClicked() && _tooltip.Player(out var state)) - _tooltip.ApplyItem(state, item); + _tooltip.ApplyItem(state, item.Item); - if (ImGui.IsItemHovered() && _tooltip.Player()) - _tooltip.CreateTooltip(item, string.Empty, true); - } - } - - private sealed class TypeColumn : ColumnString - { - public override float Width - => ImGui.CalcTextSize(FullEquipType.CrossPeinHammer.ToName()).X; - - public override string ToName(EquipItem item) - => item.Type.ToName(); - - public override void DrawColumn(EquipItem item, int _) - { - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(item.Type.ToName()); + if (Im.Item.Hovered() && _tooltip.Player()) + _tooltip.CreateTooltip(item.Item, string.Empty, true); } - public override int Compare(EquipItem lhs, EquipItem rhs) - => lhs.Type.CompareTo(rhs.Type); + protected override string ComparisonText(in UnlockCacheItem item, int globalIndex) + => item.Name.Utf16; + + protected override StringU8 DisplayText(in UnlockCacheItem item, int globalIndex) + => item.Name.Utf8; } - private sealed class SlotColumn : ColumnFlags + private sealed class TypeColumn : TextColumn { - public override float Width - => ImGui.CalcTextSize("Equip Slotmm").X; + public TypeColumn() + => Label = new StringU8("Item Type..."u8); - private EquipFlag _filterValue; + public override float ComputeWidth(IEnumerable _) + => FullEquipType.CrossPeinHammer.ToNameU8().CalculateSize().X; + protected override string ComparisonText(in UnlockCacheItem item, int globalIndex) + => string.Empty; + + public override int Compare(in UnlockCacheItem lhs, int lhsGlobalIndex, in UnlockCacheItem rhs, int rhsGlobalIndex) + => lhs.Item.Type.CompareTo(rhs.Item.Type); + + protected override StringU8 DisplayText(in UnlockCacheItem item, int globalIndex) + => item.Item.Type.ToNameU8(); + } + + private sealed class SlotColumn : FlagColumn + { public SlotColumn() { - Flags &= ~ImGuiTableColumnFlags.NoResize; - AllFlags = Values.Aggregate((a, b) => a | b); - _filterValue = AllFlags; + Flags &= ~TableColumnFlags.NoResize; + Label = new StringU8("Equip Slot"u8); } - public override void DrawColumn(EquipItem item, int idx) + public override float ComputeWidth(IEnumerable _) + => Im.Font.CalculateButtonSize(Label).X + Table.ArrowWidth; + + protected override StringU8 DisplayString(in UnlockCacheItem item, int globalIndex) { - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(ToString(item.Type.ToSlot())); + var slot = item.Slot; + return EnumData.FindFirst(i => i.Value == slot, out var pair) ? pair.Name : StringU8.Empty; } - public override EquipFlag FilterValue - => _filterValue; + protected override IReadOnlyList<(EquipFlag Value, StringU8 Name)> EnumData { get; } = + [ + (EquipFlag.Head, EquipSlot.Head.ToNameU8()), + (EquipFlag.Body, EquipSlot.Body.ToNameU8()), + (EquipFlag.Hands, EquipSlot.Hands.ToNameU8()), + (EquipFlag.Legs, EquipSlot.Legs.ToNameU8()), + (EquipFlag.Feet, EquipSlot.Feet.ToNameU8()), + (EquipFlag.Ears, new StringU8("Ears"u8)), + (EquipFlag.Neck, new StringU8("Neck"u8)), + (EquipFlag.Wrist, new StringU8("Wrists"u8)), + (EquipFlag.RFinger, new StringU8("Finger"u8)), + (EquipFlag.Mainhand, new StringU8("Mainhand"u8)), + (EquipFlag.Offhand, new StringU8("Offhand"u8)), + ]; - protected override IReadOnlyList Values - => new[] - { - EquipFlag.Mainhand, - EquipFlag.Offhand, - EquipFlag.Head, - EquipFlag.Body, - EquipFlag.Hands, - EquipFlag.Legs, - EquipFlag.Feet, - EquipFlag.Ears, - EquipFlag.Neck, - EquipFlag.Wrist, - EquipFlag.RFinger, - }; - - protected override string[] Names - => new[] - { - ToString(EquipSlot.MainHand), - ToString(EquipSlot.OffHand), - ToString(EquipSlot.Head), - ToString(EquipSlot.Body), - ToString(EquipSlot.Hands), - ToString(EquipSlot.Legs), - ToString(EquipSlot.Feet), - ToString(EquipSlot.Ears), - ToString(EquipSlot.Neck), - ToString(EquipSlot.Wrists), - ToString(EquipSlot.RFinger), - }; - - protected override void SetValue(EquipFlag value, bool enable) - => _filterValue = enable ? _filterValue | value : _filterValue & ~value; - - public override int Compare(EquipItem lhs, EquipItem rhs) - => lhs.Type.ToSlot().CompareTo(rhs.Type.ToSlot()); - - public override bool FilterFunc(EquipItem item) - => _filterValue.HasFlag(item.Type.ToSlot().ToFlag()); - - private static string ToString(EquipSlot slot) - => slot switch - { - EquipSlot.MainHand => "Mainhand", - EquipSlot.OffHand => "Offhand", - EquipSlot.Head => "Head", - EquipSlot.Body => "Body", - EquipSlot.Hands => "Hands", - EquipSlot.Legs => "Legs", - EquipSlot.Feet => "Feet", - EquipSlot.Ears => "Ears", - EquipSlot.Neck => "Neck", - EquipSlot.Wrists => "Wrists", - EquipSlot.RFinger => "Finger", - _ => string.Empty, - }; + protected override EquipFlag GetValue(in UnlockCacheItem item, int globalIndex) + => item.Slot; } - private sealed class UnlockDateColumn : Column + private sealed class UnlockDateColumn : YesNoColumn { - private readonly ItemUnlockManager _unlocks; - - public override float Width - => 110 * Im.Style.GlobalScale; - - public UnlockDateColumn(ItemUnlockManager unlocks) + public UnlockDateColumn() { - _unlocks = unlocks; - Flags &= ~ImGuiTableColumnFlags.NoResize; + Flags &= ~TableColumnFlags.NoResize; + Label = new StringU8("Unlocked"u8); + FilterLabel = Label; } - public override void DrawColumn(EquipItem item, int idx) + public override float ComputeWidth(IEnumerable allItems) + => Im.Font.CalculateButtonSize(Label).X + Table.ArrowWidth; + + public override void DrawColumn(in UnlockCacheItem item, int globalIndex) { - if (!_unlocks.IsUnlocked(item.ItemId, out var time)) + if (item.UnlockTimestamp == DateTimeOffset.MaxValue) return; - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(time == DateTimeOffset.MinValue ? "Always" : time.LocalDateTime.ToString("g")); + Im.Cursor.FrameAlign(); + Im.Text(item.UnlockText); } - public override int Compare(EquipItem lhs, EquipItem rhs) - { - var unlockedLhs = _unlocks.IsUnlocked(lhs.ItemId, out var timeLhs); - var unlockedRhs = _unlocks.IsUnlocked(rhs.ItemId, out var timeRhs); - var c1 = unlockedLhs.CompareTo(unlockedRhs); - return c1 != 0 ? c1 : timeLhs.CompareTo(timeRhs); - } + protected override bool GetValue(in UnlockCacheItem item, int globalIndex, int triEnumIndex) + => item.UnlockTimestamp != DateTimeOffset.MaxValue; + + public override int Compare(in UnlockCacheItem lhs, int lhsGlobalIndex, in UnlockCacheItem rhs, int rhsGlobalIndex) + => lhs.UnlockTimestamp.CompareTo(rhs.UnlockTimestamp); } - private sealed class ItemIdColumn : ColumnNumber + private sealed class ItemIdColumn : NumberColumn { - public override float Width - => 70 * Im.Style.GlobalScale; - - public override int ToValue(EquipItem item) - => (int)item.Id.Id; - public ItemIdColumn() - : base(ComparisonMethod.Equal) - { } + { + Label = new StringU8("Item Id..."u8); + UnscaledWidth = 70; + } + + public override uint ToValue(in UnlockCacheItem item, int globalIndex) + => item.Item.ItemId; + + protected override StringU8 DisplayNumber(in UnlockCacheItem item, int globalIndex) + => item.ItemId; + + protected override string ComparisonText(in UnlockCacheItem item, int globalIndex) + => item.ItemId; } - private sealed class ModelDataColumn : ColumnString + private sealed class ModelDataColumn : TextColumn { - private readonly ItemManager _items; - - public override float Width - => 100 * Im.Style.GlobalScale; - - public ModelDataColumn(ItemManager items) - => _items = items; - - public override void DrawColumn(EquipItem item, int _) + public ModelDataColumn() { - ImGui.AlignTextToFramePadding(); - ImGuiUtil.RightAlign(item.ModelString); - if (ImGui.IsItemHovered() - && item.Type.ValidOffhand().IsOffhandType() - && _items.ItemData.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand)) + Label = new StringU8("Model Data..."u8); + UnscaledWidth = 100; + } + + public override void DrawColumn(in UnlockCacheItem item, int globalIndex) + { + Im.Cursor.FrameAlign(); + ImEx.TextRightAligned(item.ModelString.Utf8); + if (!item.OffhandModelString.IsEmpty && Im.Item.Hovered()) { - using var tt = ImRaii.Tooltip(); - ImGui.TextUnformatted("Offhand: " + offhand.ModelString); + using var style = Im.Style.PushDefault(); + using var tt = Im.Tooltip.Begin(); + using (Im.Group()) + { + Im.Text("Offhand: "u8); + if (!item.GauntletModelString.IsEmpty) + Im.Text("Gauntlets: "u8); + } + + Im.Line.Same(); + using (Im.Group()) + { + Im.Text(item.OffhandModelString.Utf8); + if (!item.GauntletModelString.IsEmpty) + Im.Text(item.GauntletModelString.Utf8); + } } } - public override int Compare(EquipItem lhs, EquipItem rhs) - => lhs.Weapon().CompareTo(rhs.Weapon()); + public override int Compare(in UnlockCacheItem lhs, int lhsGlobalIndex, in UnlockCacheItem rhs, int rhsGlobalIndex) + => lhs.Item.Weapon().CompareTo(rhs.Item.Weapon()); - public override bool FilterFunc(EquipItem item) - { - if (FilterValue.Length == 0) - return true; + public override bool WouldBeVisible(in UnlockCacheItem item, int globalIndex) + => Filter.WouldBeVisible(item.ModelString.Utf16) + || Filter.WouldBeVisible(item.OffhandModelString.Utf16) + || Filter.WouldBeVisible(item.GauntletModelString.Utf16); - if (FilterRegex?.IsMatch(item.ModelString) ?? item.ModelString.Contains(FilterValue, StringComparison.OrdinalIgnoreCase)) - return true; + protected override string ComparisonText(in UnlockCacheItem item, int globalIndex) + => string.Empty; - if (item.Type.ValidOffhand().IsOffhandType() - && _items.ItemData.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand)) - return FilterRegex?.IsMatch(offhand.ModelString) - ?? offhand.ModelString.Contains(FilterValue, StringComparison.OrdinalIgnoreCase); - - return false; - } + protected override StringU8 DisplayText(in UnlockCacheItem item, int globalIndex) + => StringU8.Empty; } - private sealed class RequiredLevelColumn : ColumnNumber + private sealed class RequiredLevelColumn : NumberColumn { - public override float Width - => 70 * Im.Style.GlobalScale; - - public override string ToName(EquipItem item) - => item.Level.ToString(); - - public override int ToValue(EquipItem item) - => item.Level.Value; - public RequiredLevelColumn() - : base(ComparisonMethod.LessEqual) - { } + { + Label = new StringU8("Level..."u8); + UnscaledWidth = 70; + } + + public override byte ToValue(in UnlockCacheItem item, int globalIndex) + => item.Item.Level.Value; + + protected override StringU8 DisplayNumber(in UnlockCacheItem item, int globalIndex) + => item.RequiredLevel; + + protected override string ComparisonText(in UnlockCacheItem item, int globalIndex) + => item.RequiredLevel.Utf16; } - private sealed class JobColumn : ColumnFlags + private sealed class JobColumn : FlagColumn { - public override float Width - => 200 * Im.Style.GlobalScale; - private readonly JobService _jobs; - private readonly JobFlag[] _values; - private readonly string[] _names; - private JobFlag _filterValue; - - public override JobFlag FilterValue - => _filterValue; - public JobColumn(JobService jobs) + : base(false) { - _jobs = jobs; - _values = _jobs.Jobs.Ordered.Select(j => j.Flag).ToArray(); - _names = _jobs.Jobs.Ordered.Select(j => j.Abbreviation.ToString()).ToArray(); - AllFlags = _values.Aggregate((l, r) => l | r); - _filterValue = AllFlags; - Flags &= ~ImGuiTableColumnFlags.NoResize; - ComboFlags |= ImGuiComboFlags.HeightLargest; + _jobs = jobs; + Flags &= ~TableColumnFlags.NoResize; + EnumData = _jobs.Jobs.Ordered.Select(j => (j.Flag, j.Abbreviation)).ToArray(); + Label = new StringU8("Jobs"u8); + Filter = new JobFilter(this); + UnscaledWidth = 200; } - protected override bool DrawCheckbox(int idx, out bool ret) + protected override StringU8 DisplayString(in UnlockCacheItem item, int globalIndex) + => item.JobText; + + protected override IReadOnlyList<(JobFlag Value, StringU8 Name)> EnumData { get; } + + protected override JobFlag GetValue(in UnlockCacheItem item, int globalIndex) + => item.Jobs; + + private sealed class JobFilter : FlagFilter { - var job = _jobs.Jobs.Ordered[idx]; - var color = job.Role switch + public JobFilter(JobColumn parent) + : base(parent) { - Job.JobRole.Tank => 0xFFFFD0D0, - Job.JobRole.Melee => 0xFFD0D0FF, - Job.JobRole.RangedPhysical => 0xFFD0FFFF, - Job.JobRole.RangedMagical => 0xFFFFD0FF, - Job.JobRole.Healer => 0xFFD0FFD0, - Job.JobRole.Crafter => 0xFF808080, - Job.JobRole.Gatherer => 0xFFD0D0D0, - _ => ImGuiColor.Text.Get(), - }; - bool r; - using (ImGuiColor.Text.Push(color)) - { - r = base.DrawCheckbox(idx, out ret); + ComboFlags |= ComboFlags.HeightLargest; + ComboFlags &= ~ComboFlags.HeightLarge; } - if (Im.Item.RightClicked()) + public override bool WouldBeVisible(in UnlockCacheItem item, int globalIndex) + => (FilterValue & item.Jobs) is not 0; + + protected override bool DrawCheckbox(int idx) { - _filterValue = job.Flag & _filterValue; - ret = true; - r = true; + var jobs = ((JobColumn)Parent)._jobs.Jobs.Ordered; + var job = jobs[(JobId)idx]; + var color = job.Role switch + { + Job.JobRole.Tank => 0xFFFFD0D0, + Job.JobRole.Melee => 0xFFD0D0FF, + Job.JobRole.RangedPhysical => 0xFFD0FFFF, + Job.JobRole.RangedMagical => 0xFFFFD0FF, + Job.JobRole.Healer => 0xFFD0FFD0, + Job.JobRole.Crafter => 0xFF808080, + Job.JobRole.Gatherer => 0xFFD0D0D0, + _ => ImGuiColor.Text.Get(), + }; + bool r; + using (ImGuiColor.Text.Push(color)) + { + r = base.DrawCheckbox(idx); + } + + if (idx < jobs.Count - 1 && idx % 2 is 0) + Im.Line.Same(Im.Style.FrameHeight * 4); + return r; } - - ImUtf8.HoverTooltip("Right-Click to disable all other jobs."u8); - - if (idx < _names.Length - 1 && idx % 2 == 0) - ImGui.SameLine(Im.Style.FrameHeight * 4); - return r; - } - - protected override void SetValue(JobFlag value, bool enable) - => _filterValue = enable ? _filterValue | value : _filterValue & ~value; - - protected override IReadOnlyList Values - => _values; - - protected override string[] Names - => _names; - - public override int Compare(EquipItem lhs, EquipItem rhs) - => lhs.JobRestrictions.Id.CompareTo(rhs.JobRestrictions.Id); - - public override bool FilterFunc(EquipItem item) - { - if (item.JobRestrictions.Id < 2) - return true; - - if (item.JobRestrictions.Id >= _jobs.AllJobGroups.Count) - return false; - - var group = _jobs.AllJobGroups[item.JobRestrictions.Id]; - return group.Fits(FilterValue); - } - - public override void DrawColumn(EquipItem item, int idx) - { - var text = $"Unknown {item.JobRestrictions.Id}"; - if (item.JobRestrictions.Id < _jobs.AllJobGroups.Count) - { - var group = _jobs.AllJobGroups[Math.Max((int)item.JobRestrictions.Id, 1)]; - if (group.Name.Length > 0) - text = group.Name.ToString(); - } - - ImGui.TextUnformatted(text); } } - private sealed class DyableColumn : ColumnFlags + private sealed class DyableColumn : FlagColumn { - [Flags] - public enum Dyable : byte - { - No = 1, - Yes = 2, - Two = 4, - } - - private Dyable _filterValue; - public DyableColumn() { - AllFlags = Dyable.No | Dyable.Yes | Dyable.Two; - Flags &= ~ImGuiTableColumnFlags.NoResize; - _filterValue = AllFlags; + Flags &= ~TableColumnFlags.NoResize; + Label = new StringU8("Dye"u8); } - public override Dyable FilterValue - => _filterValue; + public override float ComputeWidth(IEnumerable _) + => Im.Font.CalculateButtonSize("Dye"u8).X + Table.ArrowWidth; - protected override void SetValue(Dyable value, bool enable) - => _filterValue = enable ? _filterValue | value : _filterValue & ~value; - - public override float Width - => Im.Style.FrameHeight * 2; - - public override bool FilterFunc(EquipItem item) - => GetValue(item) switch - { - 0 => _filterValue.HasFlag(Dyable.No), - ItemFlags.IsDyable2 => _filterValue.HasFlag(Dyable.Yes), - ItemFlags.IsDyable1 => _filterValue.HasFlag(Dyable.Yes), - _ => _filterValue.HasFlag(Dyable.Two), - }; - - public override int Compare(EquipItem lhs, EquipItem rhs) - => GetValue(lhs).CompareTo(GetValue(rhs)); - - public override void DrawColumn(EquipItem item, int idx) + public override void DrawColumn(in UnlockCacheItem item, int globalIndex) { - using (ImRaii.PushFont(UiBuilder.IconFont)) + var icon = item.Dyable switch { - ImGuiUtil.Center(Icon(item)); + UnlockCacheItem.Dyability.Yes => LunaStyle.TrueIcon, + UnlockCacheItem.Dyability.Two => FontAwesomeIcon.DiceTwo, + _ => LunaStyle.FalseIcon, + }; + using (AwesomeIcon.Font.Push()) + { + using var color = ImGuiColor.Text.Push(Im.Style[ImGuiColor.CheckMark]); + ImEx.TextCentered(icon.Span); } - ImGuiUtil.HoverTooltip("Whether the item is dyable, and how many slots it has."); + Im.Tooltip.OnHover("Whether the item is dyable, and how many dye slots it has."u8); } - private static string Icon(EquipItem item) - => GetValue(item) switch - { - 0 => FontAwesomeIcon.Times.ToIconString(), - ItemFlags.IsDyable2 => FontAwesomeIcon.Check.ToIconString(), - ItemFlags.IsDyable1 => FontAwesomeIcon.Check.ToIconString(), - _ => FontAwesomeIcon.DiceTwo.ToIconString(), - }; + protected override StringU8 DisplayString(in UnlockCacheItem item, int globalIndex) + => StringU8.Empty; - private static ItemFlags GetValue(EquipItem item) - => item.Flags & (ItemFlags.IsDyable1 | ItemFlags.IsDyable2); + protected override IReadOnlyList<(UnlockCacheItem.Dyability Value, StringU8 Name)> EnumData { get; } + = + [ + (UnlockCacheItem.Dyability.No, new StringU8("No"u8)), + (UnlockCacheItem.Dyability.Yes, new StringU8("Yes"u8)), + (UnlockCacheItem.Dyability.Two, new StringU8("Two"u8)), + ]; + + protected override UnlockCacheItem.Dyability GetValue(in UnlockCacheItem item, int globalIndex) + => item.Dyable; } - private sealed class TradableColumn : YesNoColumn + private sealed class TradableColumn : LunaStyle.YesNoColumn { public TradableColumn() - => Tooltip = "Whether the item is tradable."; + => Label = new StringU8("Trade"u8); - protected override bool GetValue(EquipItem item) - => item.Flags.HasFlag(ItemFlags.IsTradable); + protected override bool GetValue(in UnlockCacheItem item, int globalIndex, int triEnumIndex) + => item.Tradable; } - private sealed class CrestColumn : YesNoColumn + private sealed class CrestColumn : LunaStyle.YesNoColumn { public CrestColumn() - => Tooltip = "Whether a crest can be applied to the item.."; + => Label = new StringU8("Crest"u8); - protected override bool GetValue(EquipItem item) - => item.Flags.HasFlag(ItemFlags.IsCrestWorthy); + protected override bool GetValue(in UnlockCacheItem item, int globalIndex, int triEnumIndex) + => item.Crest; } - private sealed class ItemList(ItemManager items) : IReadOnlyCollection + public sealed class Cache : TableCache { - public IEnumerator GetEnumerator() - => items.ItemData.AllItems(true).Select(i => i.Item2).GetEnumerator(); + private new UnlockTable Parent + => (UnlockTable)base.Parent; - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); + private Guid _lastCollection; - public int Count - => items.ItemData.Primary.Count; - } + public Cache(UnlockTable parent) + : base(parent) + { + parent._unlockEvent.Subscribe(OnItemUnlock, ObjectUnlocked.Priority.UnlockTable); + parent._penumbra.ModSettingChanged += OnModSettingChanged; + Parent._favorites.FavoriteChanged += OnFavoriteChanged; + } - private void OnObjectUnlock(ObjectUnlocked.Type _1, uint _2, DateTimeOffset _3) - { - FilterDirty = true; - SortDirty = true; + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + Parent._unlockEvent.Unsubscribe(OnItemUnlock); + Parent._penumbra.ModSettingChanged -= OnModSettingChanged; + Parent._favorites.FavoriteChanged -= OnFavoriteChanged; + } + + private void OnFavoriteChanged(FavoriteManager.FavoriteType type, uint id, bool favorite) + { + if (type is not FavoriteManager.FavoriteType.Item and not FavoriteManager.FavoriteType.BonusItem) + return; + + FilterDirty = true; + SortDirty = true; + var idx = type is FavoriteManager.FavoriteType.Item + ? UnfilteredItems.IndexOf(i => i.Item.ItemId == id) + : UnfilteredItems.IndexOf(i => i.Item.Id.BonusItem == id); + if (idx >= 0) + UpdateSingleItem(idx, UnfilteredItems[idx] with { Favorite = favorite }, false); + } + + private void OnModSettingChanged(ModSettingChange type, Guid collection, string _2, bool inherited) + { + if (collection != _lastCollection) + return; + + FilterDirty = true; + SortDirty = true; + for (var i = 0; i < UnfilteredItems.Count; ++i) + { + var item = UnfilteredItems[i]; + UpdateSingleItem(i, item with { Mods = Parent._penumbra.CheckCurrentChangedItem(item.Name.Utf16) }, false); + } + } + + private void OnItemUnlock(ObjectUnlocked.Type type, uint id, DateTimeOffset timestamp) + { + if (type is not ObjectUnlocked.Type.Item) + return; + + FilterDirty = true; + SortDirty = true; + var idx = UnfilteredItems.IndexOf(i => i.Item.ItemId == id); + if (idx >= 0) + UpdateSingleItem(idx, UnfilteredItems[idx] with { UnlockTimestamp = timestamp }, false); + } + + public override void Update() + { + UpdateCollection(); + base.Update(); + } + + private void UpdateCollection() + { + var collection = Parent._penumbra.CurrentCollection.Id; + if (collection == _lastCollection) + return; + + _lastCollection = collection; + FilterDirty = true; + SortDirty = true; + for (var i = 0; i < UnfilteredItems.Count; ++i) + { + var item = UnfilteredItems[i]; + UpdateSingleItem(i, item with { Mods = Parent._penumbra.CheckCurrentChangedItem(item.Name.Utf16) }, false); + } + } } } diff --git a/Glamourer/Gui/Tabs/UnlocksTab/UnlocksTab.cs b/Glamourer/Gui/Tabs/UnlocksTab/UnlocksTab.cs index f257b7c..62a0b41 100644 --- a/Glamourer/Gui/Tabs/UnlocksTab/UnlocksTab.cs +++ b/Glamourer/Gui/Tabs/UnlocksTab/UnlocksTab.cs @@ -1,5 +1,4 @@ -using Dalamud.Bindings.ImGui; -using ImSharp; +using ImSharp; using Luna; namespace Glamourer.Gui.Tabs.UnlocksTab; @@ -46,10 +45,10 @@ public sealed class UnlocksTab : Window, ITab { DrawTypeSelection(); if (DetailMode) - _table.Draw(Im.Style.FrameHeightWithSpacing); + _table.Draw(); else _overview.Draw(); - _table.Flags |= ImGuiTableFlags.Resizable; + _table.Flags |= TableFlags.Resizable; } public override void Draw() @@ -79,7 +78,7 @@ public sealed class UnlocksTab : Window, ITab { Im.Line.Same(); if (ImEx.Icon.Button(LunaStyle.AutoResizeIcon, "Restore all columns to their original size."u8)) - _table.Flags &= ~ImGuiTableFlags.Resizable; + _table.Flags &= ~TableFlags.Resizable; } if (!IsOpen) diff --git a/Glamourer/Gui/UiHelpers.cs b/Glamourer/Gui/UiHelpers.cs index ed93853..29c296e 100644 --- a/Glamourer/Gui/UiHelpers.cs +++ b/Glamourer/Gui/UiHelpers.cs @@ -89,8 +89,7 @@ public static class UiHelpers using (Im.Disabled(locked)) { using var id = Im.Id.Push(label); - if (ImEx.TriStateCheckbox(StringU8.Empty, ref apply, ColorId.TriStateCross.Value(), ColorId.TriStateCheck.Value(), - ColorId.TriStateNeutral.Value())) + if (ImEx.TriStateCheckbox(StringU8.Empty, ref apply, ColorId.TriStateNeutral.Value(), ColorId.TriStateCheck.Value(), ColorId.TriStateCross.Value())) { (newValue, newApply) = apply switch { diff --git a/Glamourer/Services/DesignResolver.cs b/Glamourer/Services/DesignResolver.cs index d78454f..ef1e3b9 100644 --- a/Glamourer/Services/DesignResolver.cs +++ b/Glamourer/Services/DesignResolver.cs @@ -78,8 +78,8 @@ public class DesignResolver( [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool GetQuickDesign(ref DesignBase? design, ref SeString? error) { - design = quickDesignCombo.Design as Design; - if (design != null) + design = quickDesignCombo.QuickDesign; + if (design is not null) return true; error = "You do not have selected any design in the Quick Design Bar."; diff --git a/Glamourer/Unlocks/FavoriteManager.cs b/Glamourer/Unlocks/FavoriteManager.cs index 948a32b..6caa6a1 100644 --- a/Glamourer/Unlocks/FavoriteManager.cs +++ b/Glamourer/Unlocks/FavoriteManager.cs @@ -7,9 +7,9 @@ using Penumbra.GameData.Structs; namespace Glamourer.Unlocks; -public class FavoriteManager : ISavable +public sealed class FavoriteManager : ISavable { - private readonly record struct FavoriteHairStyle(Gender Gender, SubRace Race, CustomizeIndex Type, CustomizeValue Id) + public readonly record struct FavoriteHairStyle(Gender Gender, SubRace Race, CustomizeIndex Type, CustomizeValue Id) { public uint ToValue() => Id.Value | ((uint)Type << 8) | ((uint)Race << 16) | ((uint)Gender << 24); @@ -27,6 +27,17 @@ public class FavoriteManager : ISavable private readonly HashSet _favoriteHairStyles = []; private readonly HashSet _favoriteBonusItems = []; + public enum FavoriteType : byte + { + Item, + Stain, + Customization, + BonusItem, + } + + /// Event invoked with type, ID (or ) and whether the item was favorited or removed. + public event Action? FavoriteChanged; + public FavoriteManager(SaveService saveService) { _saveService = saveService; @@ -135,36 +146,44 @@ public class FavoriteManager : ISavable public bool TryAdd(ItemId item) { - if (item.Id == 0 || !_favorites.Add(item)) + if (item.Id is 0 || !_favorites.Add(item)) return false; + FavoriteChanged?.Invoke(FavoriteType.Item, item.Id, true); Save(); return true; } public bool TryAdd(BonusItemId item) { - if (item.Id == 0 || !_favoriteBonusItems.Add(item)) + if (item.Id is 0 || !_favoriteBonusItems.Add(item)) return false; + FavoriteChanged?.Invoke(FavoriteType.BonusItem, item.Id, true); Save(); return true; } public bool TryAdd(StainId stain) { - if (stain.Id == 0 || !_favoriteColors.Add(stain)) + if (stain.Id is 0 || !_favoriteColors.Add(stain)) return false; + FavoriteChanged?.Invoke(FavoriteType.Stain, stain.Id, true); Save(); return true; } public bool TryAdd(Gender gender, SubRace race, CustomizeIndex type, CustomizeValue value) { - if (!TypeAllowed(type) || !_favoriteHairStyles.Add(new FavoriteHairStyle(gender, race, type, value))) + if (!TypeAllowed(type)) return false; + var id = new FavoriteHairStyle(gender, race, type, value); + if (!_favoriteHairStyles.Add(id)) + return false; + + FavoriteChanged?.Invoke(FavoriteType.Customization, id.ToValue(), true); Save(); return true; } @@ -181,6 +200,7 @@ public class FavoriteManager : ISavable if (!_favorites.Remove(item)) return false; + FavoriteChanged?.Invoke(FavoriteType.Item, item.Id, false); Save(); return true; } @@ -190,6 +210,7 @@ public class FavoriteManager : ISavable if (!_favoriteBonusItems.Remove(item)) return false; + FavoriteChanged?.Invoke(FavoriteType.BonusItem, item.Id, false); Save(); return true; } @@ -199,15 +220,18 @@ public class FavoriteManager : ISavable if (!_favoriteColors.Remove(stain)) return false; + FavoriteChanged?.Invoke(FavoriteType.Stain, stain.Id, false); Save(); return true; } public bool Remove(Gender gender, SubRace race, CustomizeIndex type, CustomizeValue value) { - if (!_favoriteHairStyles.Remove(new FavoriteHairStyle(gender, race, type, value))) + var id = new FavoriteHairStyle(gender, race, type, value); + if (!_favoriteHairStyles.Remove(id)) return false; + FavoriteChanged?.Invoke(FavoriteType.Customization, id.ToValue(), true); Save(); return true; } diff --git a/Luna b/Luna index 3d46034..c52743f 160000 --- a/Luna +++ b/Luna @@ -1 +1 @@ -Subproject commit 3d460349da4ab862961bbb3170ccf4f0fedf0eca +Subproject commit c52743f736892dde62f39e6a2b06fde4096cdff7 diff --git a/Penumbra.GameData b/Penumbra.GameData index 6f39fad..223fb1b 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 6f39fadad5333c73cc1d90219828f169c3bbaa2a +Subproject commit 223fb1b04fee05c439b7679e7f62bc890e5d0885