Finish NPC Tab update.

This commit is contained in:
Ottermandias 2026-02-14 21:22:49 +01:00
parent ea573ccb6b
commit f2ac7e3353
14 changed files with 544 additions and 402 deletions

View file

@ -41,7 +41,6 @@ public enum DesignPanelFlag : uint
public static partial class DesignPanelFlagExtensions
{
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,12 +55,13 @@ public static partial class DesignPanelFlagExtensions
Action<DesignPanelFlag> setterExpand)
{
var checkBoxWidth = Math.Max(Im.Style.FrameHeight, Expand.CalculateSize().X);
var textWidth = AdvancedCustomization.CalculateSize().X;
var test = DesignPanelFlag.AdvancedCustomizations.ToNameU8();
var textWidth = AdvancedCustomizations_Name__GenU8.CalculateSize().X;
var tableSize = 2 * (textWidth + 2 * checkBoxWidth)
+ 10 * Im.Style.CellPadding.X
+ 2 * Im.Style.WindowPadding.X
+ 2 * Im.Style.FrameBorderThickness;
using var table = Im.Table.Begin(label, 6, TableFlags.RowBackground | TableFlags.SizingFixedFit | TableFlags.Borders,
using var table = Im.Table.Begin(label, 6, TableFlags.RowBackground | TableFlags.Borders,
new Vector2(tableSize, 6 * Im.Style.FrameHeight));
if (!table)
return;

View file

@ -1,27 +1,29 @@
using Glamourer.Designs;
using ImSharp;
using OtterGui;
using OtterGui.Widgets;
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
namespace Glamourer.Gui.Tabs.DesignTab;
public sealed class DesignColorCombo(DesignColors designColors, bool skipAutomatic) :
FilterComboCache<string>(skipAutomatic
? designColors.Keys.OrderBy(k => k)
: designColors.Keys.OrderBy(k => k).Prepend(DesignColors.AutomaticName),
MouseWheelType.Control, Glamourer.Log)
public sealed class DesignColorCombo(DesignColors designColors, bool skipAutomatic) : SimpleFilterCombo<string>(SimpleFilterType.Text)
{
protected override bool DrawSelectable(int globalIdx, bool selected)
public override StringU8 DisplayString(in string value)
=> new(value);
public override string FilterString(in string value)
=> value;
public override IEnumerable<string> GetBaseItems()
=> skipAutomatic ? designColors.Keys.OrderBy(k => k) : designColors.Keys.OrderBy(k => k).Prepend(DesignColors.AutomaticName);
public override ColorParameter TextColor(in string value)
=> value is DesignColors.AutomaticName ? ColorParameter.Default : designColors[value];
protected override bool DrawItem(in SimpleCacheItem<string> item, int globalIndex, bool selected)
{
var isAutomatic = !skipAutomatic && globalIdx is 0;
var key = Items[globalIdx];
var color = isAutomatic ? 0 : designColors[key];
using var c = ImGuiColor.Text.Push(color, !color.IsTransparent);
var ret = base.DrawSelectable(globalIdx, selected);
var isAutomatic = !skipAutomatic && globalIndex is 0;
var ret = base.DrawItem(item, globalIndex, selected);
if (isAutomatic)
ImGuiUtil.HoverTooltip(
"The automatic color uses the colors dependent on the design state, as defined in the regular color definitions.");
Im.Tooltip.OnHover(
"The automatic color uses the colors dependent on the design state, as defined in the regular color definitions."u8);
return ret;
}
}

View file

@ -167,17 +167,13 @@ public class DesignDetailTab
"Set this design to reset any temporary settings previously applied to the associated collection when it is applied through any means."u8);
ImUtf8.DrawFrameColumn("Color"u8);
var colorName = _selector.Selected!.Color.Length == 0 ? DesignColors.AutomaticName : _selector.Selected!.Color;
ImGui.TableNextColumn();
if (_colorCombo.Draw("##colorCombo", colorName, "Associate a color with this design.\n"
+ "Right-Click to revert to automatic coloring.\n"
+ "Hold Control and scroll the mousewheel to scroll.",
width.X - Im.Style.ItemSpacing.X - Im.Style.FrameHeight, Im.Style.TextHeight)
&& _colorCombo.CurrentSelection != null)
{
colorName = _colorCombo.CurrentSelection is DesignColors.AutomaticName ? string.Empty : _colorCombo.CurrentSelection;
_manager.ChangeColor(_selector.Selected!, colorName);
}
if (_colorCombo.Draw("##colorCombo"u8, _selector.Selected!.Color.Length is 0 ? DesignColors.AutomaticName : _selector.Selected!.Color,
"Associate a color with this design.\n"u8
+ "Right-Click to revert to automatic coloring.\n"u8
+ "Hold Control and scroll the mousewheel to scroll."u8,
width.X - Im.Style.ItemSpacing.X - Im.Style.FrameHeight, out var newColorName))
_manager.ChangeColor(_selector.Selected!, newColorName == DesignColors.AutomaticName ? string.Empty : newColorName);
if (Im.Item.RightClicked())
_manager.ChangeColor(_selector.Selected!, string.Empty);

View file

@ -315,30 +315,33 @@ public class MultiDesignPanel(
Im.Separator();
}
private string _colorComboSelection = string.Empty;
private void DrawMultiColor(Vector2 width, float offset)
{
ImUtf8.TextFrameAligned("Multi Colors:"u8);
ImGui.SameLine(offset, Im.Style.ItemSpacing.X);
_colorCombo.Draw("##color", _colorCombo.CurrentSelection ?? string.Empty, "Select a design color.",
Im.ContentRegion.Available.X - 2 * (width.X + Im.Style.ItemSpacing.X), Im.Style.TextHeight);
if (_colorCombo.Draw("##color"u8, _colorComboSelection, "Select a design color."u8,
Im.ContentRegion.Available.X - 2 * (width.X + Im.Style.ItemSpacing.X), out var newSelection))
_colorComboSelection = newSelection;
UpdateColorCache();
var label = _addDesigns.Count > 0
? $"Set for {_addDesigns.Count} Designs"
: "Set";
var tooltip = _addDesigns.Count == 0
? _colorCombo.CurrentSelection switch
var tooltip = _addDesigns.Count is 0
? _colorComboSelection switch
{
null => "No color specified.",
DesignColors.AutomaticName => "Use the other button to set to automatic.",
_ => $"All designs selected are already set to the color \"{_colorCombo.CurrentSelection}\".",
_ => $"All designs selected are already set to the color \"{_colorComboSelection}\".",
}
: $"Set the color of {_addDesigns.Count} designs to \"{_colorCombo.CurrentSelection}\"\n\n\t{string.Join("\n\t", _addDesigns.Select(m => m.Name.Text))}";
: $"Set the color of {_addDesigns.Count} designs to \"{_colorComboSelection}\"\n\n\t{string.Join("\n\t", _addDesigns.Select(m => m.Name.Text))}";
Im.Line.Same();
if (ImUtf8.ButtonEx(label, tooltip, width, _addDesigns.Count == 0))
if (ImEx.Button(label, width, tooltip, _addDesigns.Count is 0))
{
foreach (var design in _addDesigns)
editor.ChangeColor(design, _colorCombo.CurrentSelection!);
editor.ChangeColor(design, _colorComboSelection!);
}
label = _removeDesigns.Count > 0
@ -348,7 +351,7 @@ public class MultiDesignPanel(
? "No selected design is set to a non-automatic color."
: $"Set {_removeDesigns.Count} designs to use automatic color again:\n\n\t{string.Join("\n\t", _removeDesigns.Select(m => m.Item1.Name.Text))}";
Im.Line.Same();
if (ImUtf8.ButtonEx(label, tooltip, width, _removeDesigns.Count == 0))
if (ImEx.Button(label, width, tooltip, _removeDesigns.Count is 0))
{
foreach (var (design, _) in _removeDesigns)
editor.ChangeColor(design, string.Empty);
@ -503,7 +506,7 @@ public class MultiDesignPanel(
{
_addDesigns.Clear();
_removeDesigns.Clear();
var selection = _colorCombo.CurrentSelection ?? DesignColors.AutomaticName;
var selection = string.IsNullOrEmpty(_colorComboSelection) ? DesignColors.AutomaticName : _colorComboSelection;
foreach (var leaf in selector.SelectedPaths.OfType<DesignFileSystem.Leaf>())
{
if (leaf.Value.Color.Length > 0)

View file

@ -36,7 +36,7 @@ public class LocalNpcAppearanceData : ISavable
private Rgba32 GetColor(string color, bool favorite, ObjectKind kind)
{
if (color.Length == 0)
if (color.Length is 0)
{
if (favorite)
return ColorId.FavoriteStarOn.Value();

View file

@ -0,0 +1,14 @@
using Glamourer.GameData;
using ImSharp;
namespace Glamourer.Gui.Tabs.NpcTab;
public readonly struct NpcCacheItem(in NpcData npc, string colorText, Rgba32 color, bool favorite)
{
public readonly StringPair Name = new(npc.Name);
public readonly StringU8 Id = new($"({npc.Id})");
public readonly NpcData Npc = npc;
public readonly string ColorText = colorText;
public readonly Vector4 Color = color.ToVector();
public readonly bool Favorite = favorite;
}

View file

@ -1,44 +1,92 @@
using Glamourer.Designs;
using Glamourer.GameData;
using OtterGui.Classes;
using ImSharp;
using Luna;
namespace Glamourer.Gui.Tabs.NpcTab;
public sealed class NpcFilter(LocalNpcAppearanceData _favorites) : FilterUtility<NpcData>
public sealed class NpcFilter : TokenizedFilter<NpcFilter.TokenType, NpcCacheItem, NpcFilter.NpcFilterToken>, IUiService
{
protected override string Tooltip
=> "Filter NPC appearances for those where their names contain the given substring.\n"
+ "Enter i:[number] to filter for NPCs of certain IDs.\n"
+ "Enter c:[string] to filter for NPC appearances set to specific colors.";
protected override (LowerString, long, int) FilterChange(string input)
=> input.Length switch
{
0 => (LowerString.Empty, 0, -1),
> 1 when input[1] == ':' =>
input[0] switch
{
'i' or 'I' => input.Length == 2 ? (LowerString.Empty, 0, -1) :
long.TryParse(input.AsSpan(2), out var r) ? (LowerString.Empty, r, 1) : (LowerString.Empty, 0, -1),
'c' or 'C' => input.Length == 2 ? (LowerString.Empty, 0, -1) : (new LowerString(input[2..]), 0, 2),
_ => (new LowerString(input), 0, 0),
},
_ => (new LowerString(input), 0, 0),
};
public override bool ApplyFilter(in NpcData value)
=> FilterMode switch
{
-1 => false,
0 => Filter.IsContained(value.Name),
1 => value.Id.Id == NumericalFilter,
2 => Filter.IsContained(GetColor(value)),
_ => false, // Should never happen
};
private string GetColor(in NpcData value)
public enum TokenType : byte
{
var color = _favorites.GetColor(value);
return color.Length == 0 ? DesignColors.AutomaticName : color;
Name,
Id,
Color,
}
protected override void DrawTooltip()
{
if (!Im.Item.Hovered())
return;
using var style = Im.Style.PushDefault();
using var tt = Im.Tooltip.Begin();
Im.Text("Filter NPC appearances for those where their names contain the given substring."u8);
ImEx.TextMultiColored("Enter "u8).Then("i:[number]"u8, ColorId.TriStateCheck.Value()).Then(" to filter for NPCs of certain IDs."u8)
.End();
ImEx.TextMultiColored("Enter "u8).Then("c:[string]"u8, ColorId.TriStateCheck.Value())
.Then(" to filter for NPC appearances set to specific colors."u8).End();
}
public readonly struct NpcFilterToken() : IFilterToken<TokenType, NpcFilterToken>
{
public string Needle { get; init; } = string.Empty;
public uint ParsedNeedle { get; private init; } = 0;
public TokenType Type { get; init; }
public bool Contains(NpcFilterToken other)
{
if (Type != other.Type)
return false;
if (Type is TokenType.Id)
return ParsedNeedle == other.ParsedNeedle;
return Needle.Contains(other.Needle);
}
public static bool ConvertToken(char tokenCharacter, out TokenType type)
{
type = tokenCharacter switch
{
'i' or 'I' => TokenType.Id,
'c' or 'C' => TokenType.Color,
_ => TokenType.Name,
};
return type is not TokenType.Name;
}
public static bool AllowsNone(TokenType type)
=> false;
public static void ProcessList(List<NpcFilterToken> list)
{
for (var i = 0; i < list.Count; ++i)
{
var entry = list[i];
if (entry.Type is not TokenType.Id)
continue;
if (!uint.TryParse(entry.Needle, out var value))
list.RemoveAt(i--);
else
list[i] = new NpcFilterToken
{
ParsedNeedle = value,
Type = TokenType.Id,
};
}
}
}
protected override bool Matches(in NpcFilterToken token, in NpcCacheItem npcCacheItem)
{
return token.Type switch
{
TokenType.Name => npcCacheItem.Name.Utf16.Contains(token.Needle, StringComparison.InvariantCultureIgnoreCase),
TokenType.Id => npcCacheItem.Npc.Id == token.ParsedNeedle,
TokenType.Color => npcCacheItem.ColorText.Contains(token.Needle, StringComparison.InvariantCultureIgnoreCase),
_ => false,
};
}
protected override bool MatchesNone(TokenType type, bool negated, in NpcCacheItem npcCacheItem)
=> false;
}

View file

@ -0,0 +1,126 @@
using Dalamud.Interface.ImGuiNotification;
using Glamourer.Designs;
using ImSharp;
using Luna;
namespace Glamourer.Gui.Tabs.NpcTab;
public sealed class NpcHeader : SplitButtonHeader
{
private readonly NpcSelection _selection;
public NpcHeader(NpcSelection selection, LocalNpcAppearanceData favorites, DesignManager designs)
{
_selection = selection;
LeftButtons.AddButton(new ExportToClipboardButton(selection), 100);
LeftButtons.AddButton(new SaveAsDesignButton(selection, designs), 50);
RightButtons.AddButton(new FavoriteButton(selection, favorites), 0);
}
public override ReadOnlySpan<byte> Text
=> _selection.HasSelection ? _selection.Name : "No Selection"u8;
public override ColorParameter TextColor
=> ColorId.NormalDesign.Value();
public override void Draw(Vector2 size)
{
var color = ColorId.HeaderButtons.Value();
using var _ = ImGuiColor.Text.Push(color).Push(ImGuiColor.Border, color);
base.Draw(size with { Y = Im.Style.FrameHeight });
}
private sealed class FavoriteButton(NpcSelection selection, LocalNpcAppearanceData favorites) : BaseIconButton<AwesomeIcon>
{
public override bool HasTooltip
=> true;
public override void DrawTooltip()
=> Im.Text(
selection.Favorite ? "Remove this NPC appearance from your favorites."u8 : "Add this NPC Appearance to your favorites."u8);
public override AwesomeIcon Icon
=> LunaStyle.FavoriteIcon;
public override bool IsVisible
=> selection.HasSelection;
public override void OnClick()
=> favorites.ToggleFavorite(selection.Data);
protected override void PreDraw()
=> ImGuiColor.Text.Push(selection.Favorite ? ColorId.FavoriteStarOn.Value() : 0x80000000);
protected override void PostDraw()
=> Im.ColorDisposable.PopUnsafe();
}
private sealed class ExportToClipboardButton(NpcSelection selection) : BaseIconButton<AwesomeIcon>
{
public override bool HasTooltip
=> true;
public override void DrawTooltip()
=> Im.Text(
"Copy the current NPCs appearance to your clipboard.\nHold Control to disable applying of customizations for the copied design.\nHold Shift to disable applying of gear for the copied design."u8);
public override AwesomeIcon Icon
=> LunaStyle.ToClipboardIcon;
public override bool IsVisible
=> selection.HasSelection;
public override void OnClick()
{
try
{
var text = selection.ToBase64();
Im.Clipboard.Set(text);
}
catch (Exception ex)
{
Glamourer.Messager.NotificationMessage(ex, $"Could not copy {selection.Data.Name}'s data to clipboard.",
$"Could not copy data from NPC appearance {selection.Data.Kind} {selection.Data.Id.Id} to clipboard",
NotificationType.Error);
}
}
}
private sealed class SaveAsDesignButton(NpcSelection selection, DesignManager designs) : BaseIconButton<AwesomeIcon>
{
private StringU8 _newName = StringU8.Empty;
private DesignBase? _newDesign;
public override bool HasTooltip
=> true;
public override void DrawTooltip()
=> Im.Text(
"Save this NPCs appearance as a design.\nHold Control to disable applying of customizations for the saved design.\nHold Shift to disable applying of gear for the saved design."u8);
public override AwesomeIcon Icon
=> LunaStyle.SaveIcon;
public override bool IsVisible
=> selection.HasSelection;
public override void OnClick()
{
Im.Popup.Open("Save as Design"u8);
_newName = new StringU8(selection.Data.Name);
_newDesign = selection.ToDesignBase();
}
protected override void PostDraw()
{
if (!InputPopup.OpenName("Save as Design"u8, _newName, out var name))
return;
if (_newDesign is not null && name.Length > 0)
designs.CreateClone(_newDesign, name, true);
_newDesign = null;
_newName = StringU8.Empty;
}
}
}

View file

@ -1,7 +1,4 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Glamourer.Designs;
using Glamourer.Gui.Customization;
using Glamourer.Gui.Equipment;
@ -9,126 +6,38 @@ using Glamourer.Gui.Tabs.DesignTab;
using Glamourer.State;
using ImSharp;
using Luna;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Text;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using static Glamourer.Gui.Tabs.HeaderDrawer;
namespace Glamourer.Gui.Tabs.NpcTab;
public class NpcPanel
public sealed class NpcPanel(
Configuration config,
NpcSelection selection,
CustomizationDrawer customizeDrawer,
EquipmentDrawer equipmentDrawer,
ActorObjectManager objects,
StateManager stateManager,
LocalNpcAppearanceData favorites,
DesignColors designColors) : IPanel
{
private readonly Configuration _config;
private readonly DesignColorCombo _colorCombo;
private string _newName = string.Empty;
private DesignBase? _newDesign;
private readonly NpcSelector _selector;
private readonly LocalNpcAppearanceData _favorites;
private readonly CustomizationDrawer _customizeDrawer;
private readonly EquipmentDrawer _equipDrawer;
private readonly DesignConverter _converter;
private readonly DesignManager _designManager;
private readonly StateManager _state;
private readonly ActorObjectManager _objects;
private readonly DesignColors _colors;
private readonly Button[] _leftButtons;
private readonly Button[] _rightButtons;
private readonly DesignColorCombo _combo = new(designColors, true);
public NpcPanel(NpcSelector selector,
LocalNpcAppearanceData favorites,
CustomizationDrawer customizeDrawer,
EquipmentDrawer equipDrawer,
DesignConverter converter,
DesignManager designManager,
StateManager state,
ActorObjectManager objects,
DesignColors colors,
Configuration config)
{
_selector = selector;
_favorites = favorites;
_customizeDrawer = customizeDrawer;
_equipDrawer = equipDrawer;
_converter = converter;
_designManager = designManager;
_state = state;
_objects = objects;
_colors = colors;
_config = config;
_colorCombo = new DesignColorCombo(colors, true);
_leftButtons =
[
new ExportToClipboardButton(this),
new SaveAsDesignButton(this),
];
_rightButtons =
[
new FavoriteButton(this),
];
}
public ReadOnlySpan<byte> Id
=> "NpcPanel"u8;
public void Draw()
{
using var group = ImRaii.Group();
DrawHeader();
DrawPanel();
}
private void DrawHeader()
{
HeaderDrawer.Draw(_selector.HasSelection ? _selector.Selection.Name : "No Selection", ColorId.NormalDesign.Value().Color,
ImGuiColor.FrameBackground.Get().Color, _leftButtons, _rightButtons);
SaveDesignDrawPopup();
}
private sealed class FavoriteButton(NpcPanel panel) : Button
{
protected override string Description
=> panel._favorites.IsFavorite(panel._selector.Selection)
? "Remove this NPC appearance from your favorites."
: "Add this NPC Appearance to your favorites.";
protected override Rgba32 TextColor
=> panel._favorites.IsFavorite(panel._selector.Selection)
? ColorId.FavoriteStarOn.Value()
: 0x80000000;
protected override FontAwesomeIcon Icon
=> FontAwesomeIcon.Star;
public override bool Visible
=> panel._selector.HasSelection;
protected override void OnClick()
=> panel._favorites.ToggleFavorite(panel._selector.Selection);
}
private void SaveDesignDrawPopup()
{
if (!ImGuiUtil.OpenNameField("Save as Design", ref _newName))
using var table = Im.Table.Begin("##Panel"u8, 1, TableFlags.None, Im.ContentRegion.Available);
if (!table || !selection.HasSelection)
return;
if (_newDesign != null && _newName.Length > 0)
_designManager.CreateClone(_newDesign, _newName, true);
_newDesign = null;
_newName = string.Empty;
}
private void DrawPanel()
{
using var table = Im.Table.Begin("##Panel"u8, 1, TableFlags.BordersOuter | TableFlags.ScrollY, Im.ContentRegion.Available);
if (!table || !_selector.HasSelection)
return;
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableNextColumn();
table.SetupScrollFreeze(0, 1);
table.NextColumn();
Im.Dummy(Vector2.Zero);
DrawButtonRow();
ImGui.TableNextColumn();
table.NextColumn();
DrawCustomization();
DrawEquipment();
DrawAppearanceInfo();
@ -143,96 +52,82 @@ public class NpcPanel
private void DrawCustomization()
{
if (_config.HideDesignPanel.HasFlag(DesignPanelFlag.Customization))
if (config.HideDesignPanel.HasFlag(DesignPanelFlag.Customization))
return;
var expand = _config.AutoExpandDesignPanel.HasFlag(DesignPanelFlag.Customization);
using var h = Im.Tree.HeaderId(_selector.Selection.ModelId is 0
var expand = config.AutoExpandDesignPanel.HasFlag(DesignPanelFlag.Customization);
using var h = Im.Tree.HeaderId(selection.Data.ModelId is 0
? "Customization"u8
: $"Customization (Model Id #{_selector.Selection.ModelId})###Customization",
: $"Customization (Model Id #{selection.Data.ModelId})###Customization",
expand ? TreeNodeFlags.DefaultOpen : TreeNodeFlags.None);
if (!h)
return;
_customizeDrawer.Draw(_selector.Selection.Customize, true, true);
customizeDrawer.Draw(selection.Data.Customize, true, true);
Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
}
private void DrawEquipment()
{
using var h = DesignPanelFlag.Equipment.Header(_config);
using var h = DesignPanelFlag.Equipment.Header(config);
if (!h)
return;
_equipDrawer.Prepare();
var designData = ToDesignData();
equipmentDrawer.Prepare();
var designData = selection.ToDesignData();
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var data = new EquipDrawData(slot, designData) { Locked = true };
_equipDrawer.DrawEquip(data);
equipmentDrawer.DrawEquip(data);
}
var mainhandData = new EquipDrawData(EquipSlot.MainHand, designData) { Locked = true };
var offhandData = new EquipDrawData(EquipSlot.OffHand, designData) { Locked = true };
_equipDrawer.DrawWeapons(mainhandData, offhandData, false);
equipmentDrawer.DrawWeapons(mainhandData, offhandData, false);
Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromValue(MetaIndex.VisorState, _selector.Selection.VisorToggled));
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromValue(MetaIndex.VisorState, selection.Data.VisorToggled));
Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
}
private DesignData ToDesignData()
{
var selection = _selector.Selection;
var items = _converter.FromDrawData(selection.Equip(), selection.Mainhand, selection.Offhand, true).ToArray();
var designData = new DesignData { Customize = selection.Customize };
foreach (var (slot, item, stain) in items)
{
designData.SetItem(slot, item);
designData.SetStain(slot, stain);
}
return designData;
}
private void DrawApplyToSelf()
{
var (id, data) = _objects.PlayerData;
if (!ImUtf8.ButtonEx("Apply to Yourself"u8,
var (id, data) = objects.PlayerData;
if (!ImEx.Button("Apply to Yourself"u8, Vector2.Zero,
"Apply the current NPC appearance to your character.\nHold Control to only apply gear.\nHold Shift to only apply customizations."u8,
Vector2.Zero, !data.Valid))
!data.Valid))
return;
if (_state.GetOrCreate(id, data.Objects[0], out var state))
if (stateManager.GetOrCreate(id, data.Objects[0], out var state))
{
var design = _converter.Convert(ToDesignData(), new StateMaterialManager(), ApplicationRules.NpcFromModifiers());
_state.ApplyDesign(state, design, ApplySettings.Manual with { IsFinal = true });
var design = selection.ToDesignBase();
stateManager.ApplyDesign(state, design, ApplySettings.Manual with { IsFinal = true });
}
}
private void DrawApplyToTarget()
{
var (id, data) = _objects.TargetData;
var (id, data) = objects.TargetData;
var tt = id.IsValid
? data.Valid
? "Apply the current NPC appearance to your current target.\nHold Control to only apply gear.\nHold Shift to only apply customizations."u8
: "The current target can not be manipulated."u8
: "No valid target selected."u8;
if (!ImUtf8.ButtonEx("Apply to Target"u8, tt, Vector2.Zero, !data.Valid))
if (!ImEx.Button("Apply to Target"u8, Vector2.Zero, tt, !data.Valid))
return;
if (_state.GetOrCreate(id, data.Objects[0], out var state))
if (stateManager.GetOrCreate(id, data.Objects[0], out var state))
{
var design = _converter.Convert(ToDesignData(), new StateMaterialManager(), ApplicationRules.NpcFromModifiers());
_state.ApplyDesign(state, design, ApplySettings.Manual with { IsFinal = true });
var design = selection.ToDesignBase();
stateManager.ApplyDesign(state, design, ApplySettings.Manual with { IsFinal = true });
}
}
private void DrawAppearanceInfo()
{
using var h = DesignPanelFlag.AppearanceDetails.Header(_config);
using var h = DesignPanelFlag.AppearanceDetails.Header(config);
if (!h)
return;
@ -240,112 +135,64 @@ public class NpcPanel
if (!table)
return;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f));
table.SetupColumn("Type"u8, TableColumnFlags.WidthFixed, Im.Font.CalculateSize("Last Update Datem"u8).X);
using var style = ImStyleDouble.ButtonTextAlign.Push(new Vector2(0, 0.5f));
table.SetupColumn("Type"u8, TableColumnFlags.WidthFixed, Im.Font.CalculateButtonSize("Last Update Date"u8).X);
table.SetupColumn("Data"u8, TableColumnFlags.WidthStretch);
var selection = _selector.Selection;
CopyButton("NPC Name"u8, selection.Name);
CopyButton("NPC ID"u8, selection.Id.Id.ToString());
ImGuiUtil.DrawFrameColumn("NPC Type");
ImGui.TableNextColumn();
CopyButton(table, "NPC Name"u8, selection.Name);
CopyButton(table, "NPC ID"u8, $"{selection.Data.Id.Id}");
table.DrawFrameColumn("NPC Type"u8);
table.NextColumn();
var width = Im.ContentRegion.Available.X;
ImEx.TextFramed(selection.Kind is ObjectKind.BattleNpc ? "Battle NPC"u8 : "Event NPC"u8, new Vector2(width, 0),
ImEx.TextFramed(selection.Data.Kind is ObjectKind.BattleNpc ? "Battle NPC"u8 : "Event NPC"u8, new Vector2(width, 0),
ImGuiColor.FrameBackground.Get());
ImUtf8.DrawFrameColumn("Color"u8);
var color = _favorites.GetColor(selection);
var colorName = color.Length == 0 ? DesignColors.AutomaticName : color;
ImGui.TableNextColumn();
if (_colorCombo.Draw("##colorCombo", colorName,
"Associate a color with this NPC appearance.\n"
+ "Right-Click to revert to automatic coloring.\n"
+ "Hold Control and scroll the mousewheel to scroll.",
width - Im.Style.ItemSpacing.X - Im.Style.FrameHeight, Im.Style.TextHeight)
&& _colorCombo.CurrentSelection != null)
{
color = _colorCombo.CurrentSelection is DesignColors.AutomaticName ? string.Empty : _colorCombo.CurrentSelection;
_favorites.SetColor(selection, color);
}
table.DrawFrameColumn("Color"u8);
table.NextColumn();
var color = selection.ColorText;
if (_combo.Draw("##colorCombo"u8, selection.ColorTextU8,
"Associate a color with this NPC appearance.\n"u8
+ "Right-Click to revert to automatic coloring.\n"u8
+ "Hold Control and scroll the mousewheel to scroll."u8,
width - Im.Style.ItemInnerSpacing.X - Im.Style.FrameHeight, out var newColorText))
favorites.SetColor(selection.Data, newColorText.Item == DesignColors.AutomaticName ? string.Empty : newColorText.Item);
if (Im.Item.RightClicked())
{
_favorites.SetColor(selection, string.Empty);
favorites.SetColor(selection.Data, string.Empty);
color = string.Empty;
}
if (_colors.TryGetValue(color, out var currentColor))
if (designColors.TryGetValue(color, out var currentColor))
{
Im.Line.Same();
Im.Line.SameInner();
if (DesignColorUi.DrawColorButton($"Color associated with {color}", currentColor, out var newColor))
_colors.SetColor(color, newColor);
designColors.SetColor(color, newColor);
}
else if (color.Length is not 0)
{
Im.Line.Same();
var size = new Vector2(Im.Style.FrameHeight);
using var font = ImRaii.PushFont(UiBuilder.IconFont);
ImEx.TextFramed(LunaStyle.WarningIcon.Span, size, _colors.MissingColor);
ImUtf8.HoverTooltip("The color associated with this design does not exist."u8);
Im.Line.SameInner();
var size = new Vector2(Im.Style.FrameHeight);
using (AwesomeIcon.Font.Push())
{
ImEx.TextFramed(LunaStyle.WarningIcon.Span, size, designColors.MissingColor);
}
Im.Tooltip.OnHover("The color associated with this design does not exist."u8);
}
return;
static void CopyButton(ReadOnlySpan<byte> label, string text)
static void CopyButton(in Im.TableDisposable table, ReadOnlySpan<byte> label, Utf8StringHandler<HintStringHandlerBuffer> text)
{
ImUtf8.DrawFrameColumn(label);
ImGui.TableNextColumn();
if (ImUtf8.Button(text, new Vector2(Im.ContentRegion.Available.X, 0)))
ImUtf8.SetClipboardText(text);
ImUtf8.HoverTooltip("Click to copy to clipboard."u8);
}
}
table.DrawFrameColumn(label);
table.NextColumn();
if (!text.GetSpan(out var span))
return;
private sealed class ExportToClipboardButton(NpcPanel panel) : Button
{
protected override string Description
=> "Copy the current NPCs appearance to your clipboard.\nHold Control to disable applying of customizations for the copied design.\nHold Shift to disable applying of gear for the copied design.";
protected override FontAwesomeIcon Icon
=> FontAwesomeIcon.Copy;
public override bool Visible
=> panel._selector.HasSelection;
protected override void OnClick()
{
try
{
var data = panel.ToDesignData();
var text = panel._converter.ShareBase64(data, new StateMaterialManager(), ApplicationRules.NpcFromModifiers());
ImGui.SetClipboardText(text);
}
catch (Exception ex)
{
Glamourer.Messager.NotificationMessage(ex, $"Could not copy {panel._selector.Selection.Name}'s data to clipboard.",
$"Could not copy data from NPC appearance {panel._selector.Selection.Kind} {panel._selector.Selection.Id.Id} to clipboard",
NotificationType.Error);
}
}
}
private sealed class SaveAsDesignButton(NpcPanel panel) : Button
{
protected override string Description
=> "Save this NPCs appearance as a design.\nHold Control to disable applying of customizations for the saved design.\nHold Shift to disable applying of gear for the saved design.";
protected override FontAwesomeIcon Icon
=> FontAwesomeIcon.Save;
public override bool Visible
=> panel._selector.HasSelection;
protected override void OnClick()
{
ImGui.OpenPopup("Save as Design");
panel._newName = panel._selector.Selection.Name;
var data = panel.ToDesignData();
panel._newDesign = panel._converter.Convert(data, new StateMaterialManager(), ApplicationRules.NpcFromModifiers());
if (Im.Button(span, Im.ContentRegion.Available with { Y = 0 }))
Im.Clipboard.Set(span);
Im.Tooltip.OnHover("Click to copy to clipboard."u8);
}
}
}

View file

@ -0,0 +1,97 @@
using Glamourer.Designs;
using Glamourer.GameData;
using ImSharp;
using Luna;
namespace Glamourer.Gui.Tabs.NpcTab;
public sealed class NpcSelection : IUiService, IDisposable
{
private readonly LocalNpcAppearanceData _data;
private readonly DesignColors _colors;
private readonly DesignConverter _converter;
public NpcSelection(LocalNpcAppearanceData data, DesignColors colors, DesignConverter converter)
{
_data = data;
_colors = colors;
_converter = converter;
_data.DataChanged += OnDataChanged;
_colors.ColorChanged += OnColorChanged;
}
private void OnColorChanged()
{
if (!HasSelection)
return;
Color = _data.GetData(Data).Color.ToVector();
}
private void OnDataChanged()
{
if (!HasSelection)
return;
ColorText = _data.GetColor(Data);
ColorTextU8 = ColorText.Length is 0 ? DesignColors.AutomaticNameU8 : new StringU8(ColorText);
(var color, Favorite) = _data.GetData(Data);
Color = color.ToVector();
}
public NpcData Data { get; private set; }
public StringU8 Name { get; private set; } = StringU8.Empty;
public bool Favorite { get; private set; } = false;
public string ColorText { get; private set; } = string.Empty;
public StringU8 ColorTextU8 { get; private set; } = DesignColors.AutomaticNameU8;
public Vector4 Color { get; private set; }
public uint Id
=> Data.Id;
public bool HasSelection
=> Id is not 0;
public DesignData ToDesignData()
{
var items = _converter.FromDrawData(Data.Equip(), Data.Mainhand, Data.Offhand, true).ToArray();
var designData = new DesignData { Customize = Data.Customize };
foreach (var (slot, item, stain) in items)
{
designData.SetItem(slot, item);
designData.SetStain(slot, stain);
}
return designData;
}
public DesignBase ToDesignBase()
{
var data = ToDesignData();
return _converter.Convert(data, new StateMaterialManager(), ApplicationRules.NpcFromModifiers());
}
public string ToBase64()
{
var data = ToDesignData();
return _converter.ShareBase64(data, new StateMaterialManager(), ApplicationRules.NpcFromModifiers());
}
public void Update(in NpcCacheItem item)
{
Data = item.Npc;
Name = item.Name.Utf8;
Favorite = item.Favorite;
ColorText = item.ColorText;
ColorTextU8 = ColorText.Length > 0 ? new StringU8(ColorText) : DesignColors.AutomaticNameU8;
Color = item.Color;
}
public void Dispose()
{
_data.DataChanged -= OnDataChanged;
_colors.ColorChanged -= OnColorChanged;
}
}

View file

@ -1,92 +1,85 @@
using Glamourer.GameData;
using Dalamud.Bindings.ImGui;
using Glamourer.Designs;
using ImSharp;
using OtterGui.Extensions;
using OtterGui.Raii;
using ImGuiClip = OtterGui.ImGuiClip;
using Luna;
namespace Glamourer.Gui.Tabs.NpcTab;
public class NpcSelector : IDisposable
public sealed class NpcSelector(
NpcCustomizeSet npcs,
LocalNpcAppearanceData favorites,
NpcFilter filter,
DesignColors designColors,
NpcSelection selection) : IPanel
{
private readonly NpcCustomizeSet _npcs;
private readonly LocalNpcAppearanceData _favorites;
private readonly NpcCustomizeSet _npcs = npcs;
private readonly LocalNpcAppearanceData _favorites = favorites;
private readonly NpcFilter _filter = filter;
private readonly DesignColors _designColors = designColors;
private NpcFilter _filter;
private readonly List<int> _visibleOrdered = [];
private int _selectedGlobalIndex;
private bool _listDirty = true;
private Vector2 _defaultItemSpacing;
private float _width;
public ReadOnlySpan<byte> Id
=> "NpcSelector"u8;
public NpcSelector(NpcCustomizeSet npcs, LocalNpcAppearanceData favorites)
public void Draw()
{
_npcs = npcs;
_favorites = favorites;
_filter = new NpcFilter(_favorites);
_favorites.DataChanged += OnFavoriteChange;
Im.Cursor.Y += Im.Style.FramePadding.Y;
var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Current, () => new Cache(this));
using var clipper = new Im.ListClipper(cache.Count, Im.Style.TextHeightWithSpacing);
using var color = new Im.ColorDisposable();
foreach (var item in clipper.Iterate(cache))
{
Im.Cursor.X += Im.Style.FramePadding.X;
using var id = Im.Id.Push(item.Npc.Id.Id);
color.Push(ImGuiColor.Text, item.Color);
if (Im.Selectable(item.Name.Utf8, item.Npc.Id == selection.Id))
selection.Update(item);
color.Pop();
var size = item.Id.CalculateSize();
Im.Line.Same();
if (Im.ContentRegion.Available.X >= size.X)
{
color.Push(ImGuiColor.Text, Im.Style[ImGuiColor.TextDisabled]);
ImEx.TextRightAligned(item.Id, 0, size.X);
color.Pop();
}
else
{
Im.Tooltip.OnHover(item.Id);
Im.Line.New();
}
}
}
public void Dispose()
private NpcCacheItem CreateItem(in NpcData data)
{
_favorites.DataChanged -= OnFavoriteChange;
var colorText = _favorites.GetColor(data);
var (color, favorite) = _favorites.GetData(data);
return new NpcCacheItem(data, colorText, color, favorite);
}
private void OnFavoriteChange()
=> _listDirty = true;
public void UpdateList()
private sealed class Cache : BasicFilterCache<NpcCacheItem>
{
if (!_listDirty)
return;
private readonly NpcSelector _parent;
_listDirty = false;
_visibleOrdered.Clear();
var enumerable = _npcs.WithIndex();
if (!_filter.IsEmpty)
enumerable = enumerable.Where(d => _filter.ApplyFilter(d.Value));
var range = enumerable.OrderByDescending(d => _favorites.IsFavorite(d.Value))
.ThenBy(d => d.Index)
.Select(d => d.Index);
_visibleOrdered.AddRange(range);
}
public Cache(NpcSelector parent)
: base(parent._filter)
{
_parent = parent;
_parent._favorites.DataChanged += OnDataChange;
_parent._designColors.ColorChanged += OnDataChange;
}
public bool HasSelection
=> _selectedGlobalIndex >= 0 && _selectedGlobalIndex < _npcs.Count;
private void OnDataChange()
=> Dirty |= IManagedCache.DirtyFlags.Custom;
public NpcData Selection
=> HasSelection ? _npcs[_selectedGlobalIndex] : default;
protected override void Dispose(bool disposing)
{
_parent._favorites.DataChanged -= OnDataChange;
_parent._designColors.ColorChanged -= OnDataChange;
base.Dispose(disposing);
}
public void Draw(float width)
{
_width = width;
using var group = ImRaii.Group();
_defaultItemSpacing = Im.Style.ItemSpacing;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
if (_filter.Draw(width))
_listDirty = true;
UpdateList();
DrawSelector();
}
private void DrawSelector()
{
using var child = ImRaii.Child("##Selector", new Vector2(_width, Im.ContentRegion.Available.Y), true);
if (!child)
return;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, _defaultItemSpacing);
ImGuiClip.ClippedDraw(_visibleOrdered, DrawSelectable, Im.Style.TextHeight);
}
private void DrawSelectable(int globalIndex)
{
using var id = Im.Id.Push(globalIndex);
using var color = ImGuiColor.Text.Push(_favorites.GetData(_npcs[globalIndex]).Color);
if (ImGui.Selectable(_npcs[globalIndex].Name, _selectedGlobalIndex == globalIndex, ImGuiSelectableFlags.AllowItemOverlap))
_selectedGlobalIndex = globalIndex;
protected override IEnumerable<NpcCacheItem> GetItems()
=> _parent._npcs.Select(n => _parent.CreateItem(n)).OrderByDescending(n => n.Favorite);
}
}

View file

@ -3,18 +3,24 @@ using Luna;
namespace Glamourer.Gui.Tabs.NpcTab;
public sealed class NpcTab(NpcSelector selector, NpcPanel panel) : ITab<MainTabType>
public sealed class NpcTab : TwoPanelLayout, ITab<MainTabType>
{
public ReadOnlySpan<byte> Label
public NpcTab(NpcFilter filter, NpcSelector selector, NpcPanel panel, NpcHeader header)
{
LeftHeader = new FilterHeader<NpcCacheItem>(filter, new StringU8("Filter..."u8));
LeftPanel = selector;
LeftFooter = NopHeaderFooter.Instance;
RightHeader = header;
RightPanel = panel;
RightFooter = NopHeaderFooter.Instance;
}
public override ReadOnlySpan<byte> Label
=> "NPCs"u8;
public MainTabType Identifier
=> MainTabType.Npcs;
public void DrawContent()
{
selector.Draw(200 * Im.Style.GlobalScale);
Im.Line.Same();
panel.Draw();
}
=> Draw(TwoPanelWidth.IndeterminateRelative);
}

View file

@ -299,6 +299,28 @@ public sealed class SettingsTab(
Im.Line.New();
}
private readonly (StringU8, QdbButtons)[] _columns =
[
(new StringU8("Apply Design"u8), QdbButtons.ApplyDesign),
(new StringU8("Revert All"u8), QdbButtons.RevertAll),
(new StringU8("Revert to Auto"u8), QdbButtons.RevertAutomation),
(new StringU8("Reapply Auto"u8), QdbButtons.ReapplyAutomation),
(new StringU8("Revert Equip"u8), QdbButtons.RevertEquip),
(new StringU8("Revert Customize"u8), QdbButtons.RevertCustomize),
(new StringU8("Revert Advanced Customization"u8), QdbButtons.RevertAdvancedCustomization),
(new StringU8("Revert Advanced Dyes"u8), QdbButtons.RevertAdvancedDyes),
(new StringU8("Reset Settings"u8), QdbButtons.ResetSettings),
];
private static bool DisplayButton(QdbButtons button, bool showAuto, bool useTemporarySettings)
=> button switch
{
QdbButtons.RevertAutomation => showAuto,
QdbButtons.ReapplyAutomation => showAuto,
QdbButtons.ResetSettings => useTemporarySettings,
_ => true,
};
private void DrawQuickDesignBoxes()
{
var showAuto = config.EnableAutoDesigns;
@ -310,23 +332,11 @@ public sealed class SettingsTab(
if (!table)
return;
IEnumerable<RefTuple<ReadOnlySpan<byte>, bool, QdbButtons>> columns =
[
RefTuple.Create("Apply Design"u8, true, QdbButtons.ApplyDesign),
RefTuple.Create("Revert All"u8, true, QdbButtons.RevertAll),
RefTuple.Create("Revert to Auto"u8, showAuto, QdbButtons.RevertAutomation),
RefTuple.Create("Reapply Auto"u8, showAuto, QdbButtons.ReapplyAutomation),
RefTuple.Create("Revert Equip"u8, true, QdbButtons.RevertEquip),
RefTuple.Create("Revert Customize"u8, true, QdbButtons.RevertCustomize),
RefTuple.Create("Revert Advanced Customization"u8, true, QdbButtons.RevertAdvancedCustomization),
RefTuple.Create("Revert Advanced Dyes"u8, true, QdbButtons.RevertAdvancedDyes),
RefTuple.Create("Reset Settings"u8, config.UseTemporarySettings, QdbButtons.ResetSettings),
];
// ReSharper disable once PossibleMultipleEnumeration
foreach (var (text, display, _) in columns)
foreach (var (text, flag) in _columns)
{
if (!display)
if (!DisplayButton(flag, showAuto, config.UseTemporarySettings))
continue;
table.NextColumn();
@ -334,9 +344,9 @@ public sealed class SettingsTab(
}
// ReSharper disable once PossibleMultipleEnumeration
foreach (var (_, display, flag) in columns)
foreach (var (_, flag) in _columns)
{
if (!display)
if (!DisplayButton(flag, showAuto, config.UseTemporarySettings))
continue;
using var id = Im.Id.Push((int)flag);

2
Luna

@ -1 +1 @@
Subproject commit c52743f736892dde62f39e6a2b06fde4096cdff7
Subproject commit 042b829099c34608782c2a70da3cf189ac5f53be