From a4302c9145c33dba1f8f00b45b1a416849f6a85c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 19 Oct 2025 22:24:52 +0200 Subject: [PATCH] Update some files for Luna. --- Luna | 2 +- Penumbra.GameData | 2 +- Penumbra/Import/TexToolsImporter.Gui.cs | 5 +- Penumbra/Import/Textures/TextureDrawer.cs | 4 +- Penumbra/Mods/FeatureChecker.cs | 25 +- Penumbra/UI/AdvancedWindow/FileEditor.cs | 7 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 4 +- Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 4 +- Penumbra/UI/ChangedItemDrawer.cs | 631 ++++--- Penumbra/UI/ChangedItemIconFlag.cs | 36 +- Penumbra/UI/Classes/CollectionSelectHeader.cs | 324 ++-- Penumbra/UI/Classes/Colors.cs | 30 +- Penumbra/UI/CollectionTab/CollectionPanel.cs | 1514 ++++++++--------- Penumbra/UI/FileDialogService.cs | 315 ++-- Penumbra/UI/ImportPopup.cs | 55 +- Penumbra/UI/IncognitoService.cs | 2 +- Penumbra/UI/Knowledge/KnowledgeWindow.cs | 151 +- Penumbra/UI/Knowledge/RaceCodeTab.cs | 160 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 31 +- Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 2 +- Penumbra/UI/PredefinedTagManager.cs | 71 +- .../UI/ResourceWatcher/ResourceWatcher.cs | 4 +- 23 files changed, 1669 insertions(+), 1712 deletions(-) diff --git a/Luna b/Luna index 2542a466..1ffa8c5d 160000 --- a/Luna +++ b/Luna @@ -1 +1 @@ -Subproject commit 2542a4665aca0ee7a66ad940638ff84cd04e35b5 +Subproject commit 1ffa8c5de118f94b609f7d6352e3b63de463398c diff --git a/Penumbra.GameData b/Penumbra.GameData index fcb443b7..3a9406bc 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit fcb443b79b0954967a959aa2f498699bd1a54923 +Subproject commit 3a9406bc634228cc0815ddb6fe5e20419cafb864 diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index 5cb99d72..0fbe3f3c 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -1,4 +1,5 @@ using Dalamud.Bindings.ImGui; +using ImSharp; using OtterGui; using OtterGui.Raii; using Penumbra.Import.Structs; @@ -89,12 +90,12 @@ public partial class TexToolsImporter ImGui.TableNextColumn(); if (ex == null) { - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value()); + using var color = ImGuiColor.Text.Push(ColorId.FolderExpanded.Value()); ImGui.TextUnformatted(dir?.FullName[(_baseDirectory.FullName.Length + 1)..] ?? "Unknown Directory"); } else { - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ConflictingMod.Value()); + using var color = ImGuiColor.Text.Push(ColorId.ConflictingMod.Value()); ImGui.TextUnformatted(ex.Message); ImGuiUtil.HoverTooltip(ex.ToString()); } diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs index 5520f391..81b39d27 100644 --- a/Penumbra/Import/Textures/TextureDrawer.cs +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -125,9 +125,9 @@ public static class TextureDrawer { var (path, game, isOnPlayer) = Items[globalIdx]; bool ret; - using (var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value(), game)) + using (var color = ImGuiColor.Text.Push(ColorId.FolderExpanded.Value(), game)) { - color.Push(ImGuiCol.Text, ColorId.HandledConflictMod.Value(), isOnPlayer); + color.Push(ImGuiColor.Text, ColorId.HandledConflictMod.Value(), isOnPlayer); var equals = string.Equals(CurrentSelection.Path, path, StringComparison.OrdinalIgnoreCase); var p = game ? $"--> {path}" : path[_skipPrefix..]; ret = ImGui.Selectable(p, selected) && !equals; diff --git a/Penumbra/Mods/FeatureChecker.cs b/Penumbra/Mods/FeatureChecker.cs index 73d38e54..fc3ae522 100644 --- a/Penumbra/Mods/FeatureChecker.cs +++ b/Penumbra/Mods/FeatureChecker.cs @@ -56,25 +56,24 @@ public static class FeatureChecker var size = new Vector2((width - (numButtons - 1) * innerSpacing.X) / numButtons, 0); var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg); var textColor = ImGui.GetColorU32(ImGuiCol.TextDisabled); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, innerSpacing) - .Push(ImGuiStyleVar.FrameBorderSize, 0); - using (var color = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value()) - .Push(ImGuiCol.Button, buttonColor) - .Push(ImGuiCol.Text, textColor)) + using (var style = ImStyleBorder.Frame.Push(ColorId.FolderLine.Value(), 0) + .Push(ImStyleDouble.ItemSpacing, innerSpacing) + .Push(ImGuiColor.Button, buttonColor) + .Push(ImGuiColor.Text, textColor)) { foreach (var flag in SupportedFlags.Values) { if (mod.RequiredFeatures.HasFlag(flag)) - { - style.Push(ImGuiStyleVar.FrameBorderSize, ImUtf8.GlobalScale); - color.Pop(2); - if (ImUtf8.Button($"{flag}", size)) + { + style.Push(ImStyleSingle.FrameBorderThickness, ImUtf8.GlobalScale); + style.PopColor(2); + if (Im.Button($"{flag}", size)) editor.ChangeRequiredFeatures(mod, mod.RequiredFeatures & ~flag); - color.Push(ImGuiCol.Button, buttonColor) - .Push(ImGuiCol.Text, textColor); - style.Pop(); + style.Push(ImGuiColor.Button, buttonColor) + .Push(ImGuiColor.Text, textColor); + style.PopStyle(); } - else if (ImUtf8.Button($"{flag}", size)) + else if (Im.Button($"{flag}", size)) { editor.ChangeRequiredFeatures(mod, mod.RequiredFeatures | flag); } diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index 62dbfb51..cc9ee136 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -1,5 +1,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; +using Dalamud.Interface.Colors; using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; using ImSharp; @@ -297,7 +298,7 @@ public class FileEditor( { var file = Items[globalIdx]; bool ret; - using (var c = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(), file.IsOnPlayer)) + using (var c = ImGuiColor.Text.Push(ColorId.HandledConflictMod.Value(), file.IsOnPlayer)) { ret = ImGui.Selectable(file.RelPath.ToString(), selected); } @@ -313,7 +314,7 @@ public class FileEditor( ImGui.TableNextColumn(); ImUtf8.Text(gamePath.Path.Span); ImGui.TableNextColumn(); - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); + using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value()); ImGui.TextUnformatted(option.GetFullName()); } } @@ -321,7 +322,7 @@ public class FileEditor( if (file.SubModUsage.Count > 0) { Im.Line.Same(); - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); + using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value()); ImGuiUtil.RightAlign(file.SubModUsage[0].Item2.Path.ToString()); } diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 57be786f..c039ab34 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -159,9 +159,9 @@ public class ItemSwapTab : IDisposable, ITab, IUiService { var (_, inMod, inCollection) = Items[globalIdx]; using var color = inMod - ? ImRaii.PushColor(ImGuiCol.Text, ColorId.ResTreeLocalPlayer.Value()) + ? ImGuiColor.Text.Push(ColorId.ResTreeLocalPlayer.Value()) : inCollection.Count > 0 - ? ImRaii.PushColor(ImGuiCol.Text, ColorId.ResTreeNonNetworked.Value()) + ? ImGuiColor.Text.Push(ColorId.ResTreeNonNetworked.Value()) : null; var ret = base.DrawSelectable(globalIdx, selected); if (inCollection.Count > 0) diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index 72023e0c..ba99053c 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -58,12 +58,12 @@ public class ModMergeTab(ModMerger modMerger) : Luna.IUiService ImGui.SameLine(0, 0); if (size - textSize < minComboSize) { - ImUtf8.Text("selected mod"u8, ColorId.FolderLine.Value()); + Im.Text("selected mod"u8, ColorId.FolderLine.Value()); ImUtf8.HoverTooltip(modMerger.MergeFromMod!.Name); } else { - ImUtf8.Text(modMerger.MergeFromMod!.Name, ColorId.FolderLine.Value()); + Im.Text(modMerger.MergeFromMod!.Name, ColorId.FolderLine.Value()); } ImGui.SameLine(0, 0); diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 5356b12b..0776954f 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -1,317 +1,314 @@ -using Dalamud.Interface; -using Dalamud.Interface.Textures; -using Dalamud.Interface.Textures.TextureWraps; -using Dalamud.Plugin.Services; -using Dalamud.Utility; -using Dalamud.Bindings.ImGui; -using ImSharp; -using Lumina.Data.Files; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Text; -using Penumbra.Communication; -using Penumbra.GameData.Data; -using Penumbra.Services; -using Penumbra.UI.Classes; -using MouseButton = Penumbra.Api.Enums.MouseButton; - -namespace Penumbra.UI; - -public class ChangedItemDrawer : IDisposable, Luna.IUiService -{ - private static readonly string[] LowerNames = ChangedItemFlagExtensions.Order.Select(f => f.ToDescription().ToLowerInvariant()).ToArray(); - - public static bool TryParseIndex(ReadOnlySpan input, out ChangedItemIconFlag slot) - { - // Handle numeric cases before TryParse because numbers - // are not logical otherwise. - if (int.TryParse(input, out var idx)) - { - // We assume users will use 1-based index, but if they enter 0, just use the first. - if (idx == 0) - { - slot = ChangedItemFlagExtensions.Order[0]; - return true; - } - - // Use 1-based index. - --idx; - if (idx >= 0 && idx < ChangedItemFlagExtensions.Order.Count) - { - slot = ChangedItemFlagExtensions.Order[idx]; - return true; - } - } - - slot = 0; - return false; - } - - public static bool TryParsePartial(string lowerInput, out ChangedItemIconFlag slot) - { - if (TryParseIndex(lowerInput, out slot)) - return true; - - slot = 0; - foreach (var (item, flag) in LowerNames.Zip(ChangedItemFlagExtensions.Order)) - { - if (item.Contains(lowerInput, StringComparison.Ordinal)) - slot |= flag; - } - - return slot != 0; - } - - - private readonly Configuration _config; - private readonly CommunicatorService _communicator; - private readonly Dictionary _icons = new(16); - private float _smallestIconWidth; - - public static Vector2 TypeFilterIconSize - => new(2 * ImGui.GetTextLineHeight()); - - public ChangedItemDrawer(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider, CommunicatorService communicator, - Configuration config) - { - uiBuilder.RunWhenUiPrepared(() => CreateEquipSlotIcons(uiBuilder, gameData, textureProvider), true); - _communicator = communicator; - _config = config; - } - - public void Dispose() - { - foreach (var wrap in _icons.Values.Distinct()) - wrap.Dispose(); - _icons.Clear(); - } - - /// Check if a changed item should be drawn based on its category. - public bool FilterChangedItem(string name, IIdentifiedObjectData data, string filter) - => (_config.Ephemeral.ChangedItemFilter == ChangedItemFlagExtensions.AllFlags - || _config.Ephemeral.ChangedItemFilter.HasFlag(data.GetIcon().ToFlag())) - && (filter.Length is 0 || !data.IsFilteredOut(name, filter)); - - /// Draw the icon corresponding to the category of a changed item. - public void DrawCategoryIcon(IIdentifiedObjectData data, float height) - => DrawCategoryIcon(data.GetIcon().ToFlag(), height); - - public void DrawCategoryIcon(ChangedItemIconFlag iconFlagType) - => DrawCategoryIcon(iconFlagType, ImGui.GetFrameHeight()); - - public void DrawCategoryIcon(ChangedItemIconFlag iconFlagType, float height) - { - if (!_icons.TryGetValue(iconFlagType, out var icon)) - { - ImGui.Dummy(new Vector2(height)); - return; - } - - ImGui.Image(icon.Handle, new Vector2(height)); - if (ImGui.IsItemHovered()) - { - using var tt = ImRaii.Tooltip(); - ImGui.Image(icon.Handle, new Vector2(_smallestIconWidth)); - Im.Line.Same(); - ImGuiUtil.DrawTextButton(iconFlagType.ToDescription(), new Vector2(0, _smallestIconWidth), 0); - } - } - - public void ChangedItemHandling(IIdentifiedObjectData data, bool leftClicked) - { - var ret = leftClicked ? MouseButton.Left : MouseButton.None; - ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret; - ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret; - if (ret != MouseButton.None) - _communicator.ChangedItemClick.Invoke(new ChangedItemClick.Arguments(ret, data)); - if (!ImGui.IsItemHovered()) - return; - - using var tt = ImUtf8.Tooltip(); - if (data.Count == 1) - ImUtf8.Text("This item is changed through a single effective change.\n"); - else - ImUtf8.Text($"This item is changed through {data.Count} distinct effective changes.\n"); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3 * ImUtf8.GlobalScale); - ImGui.Separator(); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3 * ImUtf8.GlobalScale); - _communicator.ChangedItemHover.Invoke(new ChangedItemHover.Arguments(data)); - } - - /// Draw the model information, right-justified. - public static void DrawModelData(IIdentifiedObjectData data, float height) - { - var additionalData = data.AdditionalData; - if (additionalData.Length == 0) - return; - - Im.Line.Same(); - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (height - ImGui.GetTextLineHeight()) / 2); - ImUtf8.TextRightAligned(additionalData, ImGui.GetStyle().ItemInnerSpacing.X); - } - - /// Draw the model information, right-justified. - public static void DrawModelData(ReadOnlySpan text, float height) - { - if (text.Length == 0) - return; - - Im.Line.Same(); - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (height - ImGui.GetTextLineHeight()) / 2); - ImUtf8.TextRightAligned(text, ImGui.GetStyle().ItemInnerSpacing.X); - } - - /// Draw a header line with the different icon types to filter them. - public void DrawTypeFilter() - { - if (_config.HideChangedItemFilters) - return; - - var typeFilter = _config.Ephemeral.ChangedItemFilter; - if (DrawTypeFilter(ref typeFilter)) - { - _config.Ephemeral.ChangedItemFilter = typeFilter; - _config.Ephemeral.Save(); - } - } - - /// Draw a header line with the different icon types to filter them. - public bool DrawTypeFilter(ref ChangedItemIconFlag typeFilter) - { - var ret = false; - using var _ = ImRaii.PushId("ChangedItemIconFilter"); - var size = TypeFilterIconSize; - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - - - foreach (var iconType in ChangedItemFlagExtensions.Order) - { - ret |= DrawIcon(iconType, ref typeFilter); - Im.Line.Same(); - } - - ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X); - ImGui.Image(_icons[ChangedItemFlagExtensions.AllFlags].Handle, size, Vector2.Zero, Vector2.One, - typeFilter switch - { - 0 => new Vector4(0.6f, 0.3f, 0.3f, 1f), - ChangedItemFlagExtensions.AllFlags => new Vector4(0.75f, 0.75f, 0.75f, 1f), - _ => new Vector4(0.5f, 0.5f, 1f, 1f), - }); - if (ImGui.IsItemClicked()) - { - typeFilter = typeFilter == ChangedItemFlagExtensions.AllFlags ? 0 : ChangedItemFlagExtensions.AllFlags; - ret = true; - } - - return ret; - - bool DrawIcon(ChangedItemIconFlag type, ref ChangedItemIconFlag typeFilter) - { - var localRet = false; - var icon = _icons[type]; - var flag = typeFilter.HasFlag(type); - ImGui.Image(icon.Handle, size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f)); - if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) - { - typeFilter = flag ? typeFilter & ~type : typeFilter | type; - localRet = true; - } - - using var popup = ImRaii.ContextPopupItem(type.ToString()); - if (popup) - if (ImGui.MenuItem("Enable Only This")) - { - typeFilter = type; - localRet = true; - ImGui.CloseCurrentPopup(); - } - - if (ImGui.IsItemHovered()) - { - using var tt = ImRaii.Tooltip(); - ImGui.Image(icon.Handle, new Vector2(_smallestIconWidth)); - Im.Line.Same(); - ImGuiUtil.DrawTextButton(type.ToDescription(), new Vector2(0, _smallestIconWidth), 0); - } - - return localRet; - } - } - - /// Initialize the icons. - private bool CreateEquipSlotIcons(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider) - { - using var equipTypeIcons = uiBuilder.LoadUld("ui/uld/ArmouryBoard.uld"); - - if (!equipTypeIcons.Valid) - return false; - - void Add(ChangedItemIconFlag icon, IDalamudTextureWrap? tex) - { - if (tex != null) - _icons.Add(icon, tex); - } - - Add(ChangedItemIconFlag.Mainhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0)); - Add(ChangedItemIconFlag.Head, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1)); - Add(ChangedItemIconFlag.Body, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2)); - Add(ChangedItemIconFlag.Hands, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3)); - Add(ChangedItemIconFlag.Legs, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5)); - Add(ChangedItemIconFlag.Feet, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6)); - Add(ChangedItemIconFlag.Offhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7)); - Add(ChangedItemIconFlag.Ears, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8)); - Add(ChangedItemIconFlag.Neck, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9)); - Add(ChangedItemIconFlag.Wrists, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10)); - Add(ChangedItemIconFlag.Finger, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11)); - Add(ChangedItemIconFlag.Monster, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062044_hr1.tex")!)); - Add(ChangedItemIconFlag.Demihuman, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062043_hr1.tex")!)); - Add(ChangedItemIconFlag.Customization, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062045_hr1.tex")!)); - Add(ChangedItemIconFlag.Action, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062001_hr1.tex")!)); - Add(ChangedItemIconFlag.Emote, LoadEmoteTexture(gameData, textureProvider)); - Add(ChangedItemIconFlag.Unknown, LoadUnknownTexture(gameData, textureProvider)); - Add(ChangedItemFlagExtensions.AllFlags, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/114000/114052_hr1.tex")!)); - - _smallestIconWidth = _icons.Values.Min(i => i.Width); - - return true; - } - - private static IDalamudTextureWrap? LoadUnknownTexture(IDataManager gameData, ITextureProvider textureProvider) - { - var unk = gameData.GetFile("ui/uld/levelup2_hr1.tex"); - if (unk == null) - return null; - - var image = unk.GetRgbaImageData(); - var bytes = new byte[unk.Header.Height * unk.Header.Height * 4]; - var diff = 2 * (unk.Header.Height - unk.Header.Width); - for (var y = 0; y < unk.Header.Height; ++y) - image.AsSpan(4 * y * unk.Header.Width, 4 * unk.Header.Width).CopyTo(bytes.AsSpan(4 * y * unk.Header.Height + diff)); - - return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(unk.Header.Height, unk.Header.Height), bytes, "Penumbra.UnkItemIcon"); - } - - private static unsafe IDalamudTextureWrap? LoadEmoteTexture(IDataManager gameData, ITextureProvider textureProvider) - { - var emote = gameData.GetFile("ui/icon/000000/000019_hr1.tex"); - if (emote == null) - return null; - - var image2 = emote.GetRgbaImageData(); - fixed (byte* ptr = image2) - { - var color = (uint*)ptr; - for (var i = 0; i < image2.Length / 4; ++i) - { - if (color[i] == 0xFF000000) - image2[i * 4 + 3] = 0; - } - } - - return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(emote.Header.Width, emote.Header.Height), image2, - "Penumbra.EmoteItemIcon"); - } -} +using Dalamud.Interface; +using Dalamud.Interface.Textures; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Plugin.Services; +using Dalamud.Utility; +using ImSharp; +using Lumina.Data.Files; +using Luna; +using Penumbra.Communication; +using Penumbra.GameData.Data; +using Penumbra.Services; +using Penumbra.UI.Classes; +using MouseButton = Penumbra.Api.Enums.MouseButton; + +namespace Penumbra.UI; + +public class ChangedItemDrawer : IDisposable, IUiService +{ + private static readonly string[] LowerNames = ChangedItemFlagExtensions.Order.Select(f => f.ToDescription().ToString().ToLowerInvariant()).ToArray(); + + public static bool TryParseIndex(ReadOnlySpan input, out ChangedItemIconFlag slot) + { + // Handle numeric cases before TryParse because numbers + // are not logical otherwise. + if (int.TryParse(input, out var idx)) + { + // We assume users will use 1-based index, but if they enter 0, just use the first. + if (idx == 0) + { + slot = ChangedItemFlagExtensions.Order[0]; + return true; + } + + // Use 1-based index. + --idx; + if (idx >= 0 && idx < ChangedItemFlagExtensions.Order.Count) + { + slot = ChangedItemFlagExtensions.Order[idx]; + return true; + } + } + + slot = 0; + return false; + } + + public static bool TryParsePartial(string lowerInput, out ChangedItemIconFlag slot) + { + if (TryParseIndex(lowerInput, out slot)) + return true; + + slot = 0; + foreach (var (item, flag) in LowerNames.Zip(ChangedItemFlagExtensions.Order)) + { + if (item.Contains(lowerInput, StringComparison.Ordinal)) + slot |= flag; + } + + return slot != 0; + } + + + private readonly Configuration _config; + private readonly CommunicatorService _communicator; + private readonly Dictionary _icons = new(16); + private float _smallestIconWidth; + + public static Vector2 TypeFilterIconSize + => new(2 * Im.Style.TextHeight); + + public ChangedItemDrawer(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider, CommunicatorService communicator, + Configuration config) + { + uiBuilder.RunWhenUiPrepared(() => CreateEquipSlotIcons(uiBuilder, gameData, textureProvider), true); + _communicator = communicator; + _config = config; + } + + public void Dispose() + { + foreach (var wrap in _icons.Values.Distinct()) + wrap.Dispose(); + _icons.Clear(); + } + + /// Check if a changed item should be drawn based on its category. + public bool FilterChangedItem(string name, IIdentifiedObjectData data, string filter) + => (_config.Ephemeral.ChangedItemFilter == ChangedItemFlagExtensions.AllFlags + || _config.Ephemeral.ChangedItemFilter.HasFlag(data.GetIcon().ToFlag())) + && (filter.Length is 0 || !data.IsFilteredOut(name, filter)); + + /// Draw the icon corresponding to the category of a changed item. + public void DrawCategoryIcon(IIdentifiedObjectData data, float height) + => DrawCategoryIcon(data.GetIcon().ToFlag(), height); + + public void DrawCategoryIcon(ChangedItemIconFlag iconFlagType) + => DrawCategoryIcon(iconFlagType, Im.Style.FrameHeight); + + public void DrawCategoryIcon(ChangedItemIconFlag iconFlagType, float height) + { + if (!_icons.TryGetValue(iconFlagType, out var icon)) + { + Im.Dummy(0, height); + return; + } + + Im.Image.Draw(icon.Id(), new Vector2(height)); + if (Im.Item.Hovered()) + { + using var tt = Im.Tooltip.Begin(); + Im.Image.Draw(icon.Id(), new Vector2(_smallestIconWidth)); + Im.Line.Same(); + ImEx.TextFramed(iconFlagType.ToDescription(), new Vector2(0, _smallestIconWidth), 0); + } + } + + public void ChangedItemHandling(IIdentifiedObjectData data, bool leftClicked) + { + var ret = leftClicked ? MouseButton.Left : MouseButton.None; + ret = Im.Item.Clicked(ImSharp.MouseButton.Right) ? MouseButton.Right : ret; + ret = Im.Item.Clicked(ImSharp.MouseButton.Middle) ? MouseButton.Middle : ret; + if (ret != MouseButton.None) + _communicator.ChangedItemClick.Invoke(new ChangedItemClick.Arguments(ret, data)); + if (!Im.Item.Hovered()) + return; + + using var tt = Im.Tooltip.Begin(); + if (data.Count == 1) + Im.Text("This item is changed through a single effective change.\n"u8); + else + Im.Text($"This item is changed through {data.Count} distinct effective changes.\n"); + Im.Cursor.Y += 3 * Im.Style.GlobalScale; + Im.Separator(); + Im.Cursor.Y += 3 * Im.Style.GlobalScale; + _communicator.ChangedItemHover.Invoke(new ChangedItemHover.Arguments(data)); + } + + /// Draw the model information, right-justified. + public static void DrawModelData(IIdentifiedObjectData data, float height) + { + var additionalData = data.AdditionalData; + if (additionalData.Length == 0) + return; + + Im.Line.Same(); + using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value()); + Im.Cursor.Y += height - Im.Style.TextHeight / 2; + ImEx.TextRightAligned(additionalData, Im.Style.ItemInnerSpacing.X); + } + + /// Draw the model information, right-justified. + public static void DrawModelData(ReadOnlySpan text, float height) + { + if (text.Length == 0) + return; + + Im.Line.Same(); + using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value()); + Im.Cursor.Y += height - Im.Style.TextHeight / 2; + ImEx.TextRightAligned(text, Im.Style.ItemInnerSpacing.X); + } + + /// Draw a header line with the different icon types to filter them. + public void DrawTypeFilter() + { + if (_config.HideChangedItemFilters) + return; + + var typeFilter = _config.Ephemeral.ChangedItemFilter; + if (DrawTypeFilter(ref typeFilter)) + { + _config.Ephemeral.ChangedItemFilter = typeFilter; + _config.Ephemeral.Save(); + } + } + + /// Draw a header line with the different icon types to filter them. + public bool DrawTypeFilter(ref ChangedItemIconFlag typeFilter) + { + var ret = false; + using var _ = Im.Id.Push("ChangedItemIconFilter"u8); + var size = TypeFilterIconSize; + using var style = ImStyleDouble.ItemSpacing.Push(Vector2.Zero); + + + foreach (var iconType in ChangedItemFlagExtensions.Order) + { + ret |= DrawIcon(iconType, ref typeFilter); + Im.Line.Same(); + } + + Im.Cursor.X = Im.ContentRegion.Maximum.X - size.X; + Im.Image.Draw(_icons[ChangedItemFlagExtensions.AllFlags].Id(), size, Vector2.Zero, Vector2.One, + typeFilter switch + { + 0 => new Vector4(0.6f, 0.3f, 0.3f, 1f), + ChangedItemFlagExtensions.AllFlags => new Vector4(0.75f, 0.75f, 0.75f, 1f), + _ => new Vector4(0.5f, 0.5f, 1f, 1f), + }); + if (Im.Item.Clicked()) + { + typeFilter = typeFilter == ChangedItemFlagExtensions.AllFlags ? 0 : ChangedItemFlagExtensions.AllFlags; + ret = true; + } + + return ret; + + bool DrawIcon(ChangedItemIconFlag type, ref ChangedItemIconFlag typeFilter) + { + var localRet = false; + var icon = _icons[type]; + var flag = typeFilter.HasFlag(type); + Im.Image.Draw(icon.Id(), size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f)); + if (Im.Item.Clicked()) + { + typeFilter = flag ? typeFilter & ~type : typeFilter | type; + localRet = true; + } + + using var popup = Im.Popup.BeginContextItem($"{type}"); + if (popup) + if (Im.Menu.Item("Enable Only This"u8)) + { + typeFilter = type; + localRet = true; + Im.Popup.CloseCurrent(); + } + + if (Im.Item.Hovered()) + { + using var tt = Im.Tooltip.Begin(); + Im.Image.Draw(icon.Id(), new Vector2(_smallestIconWidth)); + Im.Line.Same(); + ImEx.TextFramed(type.ToDescription(), new Vector2(0, _smallestIconWidth), 0); + } + + return localRet; + } + } + + /// Initialize the icons. + private bool CreateEquipSlotIcons(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider) + { + using var equipTypeIcons = uiBuilder.LoadUld("ui/uld/ArmouryBoard.uld"); + + if (!equipTypeIcons.Valid) + return false; + + void Add(ChangedItemIconFlag icon, IDalamudTextureWrap? tex) + { + if (tex != null) + _icons.Add(icon, tex); + } + + Add(ChangedItemIconFlag.Mainhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0)); + Add(ChangedItemIconFlag.Head, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1)); + Add(ChangedItemIconFlag.Body, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2)); + Add(ChangedItemIconFlag.Hands, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3)); + Add(ChangedItemIconFlag.Legs, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5)); + Add(ChangedItemIconFlag.Feet, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6)); + Add(ChangedItemIconFlag.Offhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7)); + Add(ChangedItemIconFlag.Ears, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8)); + Add(ChangedItemIconFlag.Neck, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9)); + Add(ChangedItemIconFlag.Wrists, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10)); + Add(ChangedItemIconFlag.Finger, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11)); + Add(ChangedItemIconFlag.Monster, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062044_hr1.tex")!)); + Add(ChangedItemIconFlag.Demihuman, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062043_hr1.tex")!)); + Add(ChangedItemIconFlag.Customization, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062045_hr1.tex")!)); + Add(ChangedItemIconFlag.Action, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062001_hr1.tex")!)); + Add(ChangedItemIconFlag.Emote, LoadEmoteTexture(gameData, textureProvider)); + Add(ChangedItemIconFlag.Unknown, LoadUnknownTexture(gameData, textureProvider)); + Add(ChangedItemFlagExtensions.AllFlags, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/114000/114052_hr1.tex")!)); + + _smallestIconWidth = _icons.Values.Min(i => i.Width); + + return true; + } + + private static IDalamudTextureWrap? LoadUnknownTexture(IDataManager gameData, ITextureProvider textureProvider) + { + var unk = gameData.GetFile("ui/uld/levelup2_hr1.tex"); + if (unk == null) + return null; + + var image = unk.GetRgbaImageData(); + var bytes = new byte[unk.Header.Height * unk.Header.Height * 4]; + var diff = 2 * (unk.Header.Height - unk.Header.Width); + for (var y = 0; y < unk.Header.Height; ++y) + image.AsSpan(4 * y * unk.Header.Width, 4 * unk.Header.Width).CopyTo(bytes.AsSpan(4 * y * unk.Header.Height + diff)); + + return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(unk.Header.Height, unk.Header.Height), bytes, "Penumbra.UnkItemIcon"); + } + + private static unsafe IDalamudTextureWrap? LoadEmoteTexture(IDataManager gameData, ITextureProvider textureProvider) + { + var emote = gameData.GetFile("ui/icon/000000/000019_hr1.tex"); + if (emote == null) + return null; + + var image2 = emote.GetRgbaImageData(); + fixed (byte* ptr = image2) + { + var color = (uint*)ptr; + for (var i = 0; i < image2.Length / 4; ++i) + { + if (color[i] == 0xFF000000) + image2[i * 4 + 3] = 0; + } + } + + return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(emote.Header.Width, emote.Header.Height), image2, + "Penumbra.EmoteItemIcon"); + } +} diff --git a/Penumbra/UI/ChangedItemIconFlag.cs b/Penumbra/UI/ChangedItemIconFlag.cs index fc7073f2..adb901b7 100644 --- a/Penumbra/UI/ChangedItemIconFlag.cs +++ b/Penumbra/UI/ChangedItemIconFlag.cs @@ -52,26 +52,26 @@ public static class ChangedItemFlagExtensions public static readonly int NumCategories = Order.Count; public const ChangedItemIconFlag DefaultFlags = AllFlags & ~ChangedItemIconFlag.Offhand; - public static string ToDescription(this ChangedItemIconFlag iconFlag) + public static ReadOnlySpan ToDescription(this ChangedItemIconFlag iconFlag) => iconFlag switch { - ChangedItemIconFlag.Head => EquipSlot.Head.ToName(), - ChangedItemIconFlag.Body => EquipSlot.Body.ToName(), - ChangedItemIconFlag.Hands => EquipSlot.Hands.ToName(), - ChangedItemIconFlag.Legs => EquipSlot.Legs.ToName(), - ChangedItemIconFlag.Feet => EquipSlot.Feet.ToName(), - ChangedItemIconFlag.Ears => EquipSlot.Ears.ToName(), - ChangedItemIconFlag.Neck => EquipSlot.Neck.ToName(), - ChangedItemIconFlag.Wrists => EquipSlot.Wrists.ToName(), - ChangedItemIconFlag.Finger => "Ring", - ChangedItemIconFlag.Monster => "Monster", - ChangedItemIconFlag.Demihuman => "Demi-Human", - ChangedItemIconFlag.Customization => "Customization", - ChangedItemIconFlag.Action => "Action", - ChangedItemIconFlag.Emote => "Emote", - ChangedItemIconFlag.Mainhand => "Weapon (Mainhand)", - ChangedItemIconFlag.Offhand => "Weapon (Offhand)", - _ => "Other", + ChangedItemIconFlag.Head => EquipSlot.Head.ToNameU8(), + ChangedItemIconFlag.Body => EquipSlot.Body.ToNameU8(), + ChangedItemIconFlag.Hands => EquipSlot.Hands.ToNameU8(), + ChangedItemIconFlag.Legs => EquipSlot.Legs.ToNameU8(), + ChangedItemIconFlag.Feet => EquipSlot.Feet.ToNameU8(), + ChangedItemIconFlag.Ears => EquipSlot.Ears.ToNameU8(), + ChangedItemIconFlag.Neck => EquipSlot.Neck.ToNameU8(), + ChangedItemIconFlag.Wrists => EquipSlot.Wrists.ToNameU8(), + ChangedItemIconFlag.Finger => "Ring"u8, + ChangedItemIconFlag.Monster => "Monster"u8, + ChangedItemIconFlag.Demihuman => "Demi-Human"u8, + ChangedItemIconFlag.Customization => "Customization"u8, + ChangedItemIconFlag.Action => "Action"u8, + ChangedItemIconFlag.Emote => "Emote"u8, + ChangedItemIconFlag.Mainhand => "Weapon (Mainhand)"u8, + ChangedItemIconFlag.Offhand => "Weapon (Offhand)"u8, + _ => "Other"u8, }; public static ChangedItemIcon ToApiIcon(this ChangedItemIconFlag iconFlag) diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index fb8a95be..a829d9e5 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -1,160 +1,164 @@ -using Dalamud.Interface; -using Dalamud.Bindings.ImGui; -using ImSharp; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Text; -using Penumbra.Collections; -using Penumbra.Collections.Manager; -using Penumbra.Interop.PathResolving; -using Penumbra.Mods; -using Penumbra.UI.CollectionTab; - -namespace Penumbra.UI.Classes; - -public class CollectionSelectHeader( - CollectionManager collectionManager, - TutorialService tutorial, - ModSelection selection, - CollectionResolver resolver, - Configuration config, - CollectionCombo combo) - : Luna.IUiService -{ - private readonly ActiveCollections _activeCollections = collectionManager.Active; - - /// Draw the header line that can quick switch between collections. - public void Draw(bool spacing) - { - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0) - .Push(ImGuiStyleVar.ItemSpacing, new Vector2(0, spacing ? ImGui.GetStyle().ItemSpacing.Y : 0)); - DrawTemporaryCheckbox(); - Im.Line.Same(); - var comboWidth = ImGui.GetContentRegionAvail().X / 4f; - var buttonSize = new Vector2(comboWidth * 3f / 4f, 0f); - using (var _ = ImRaii.Group()) - { - DrawCollectionButton(buttonSize, GetDefaultCollectionInfo(), 1); - DrawCollectionButton(buttonSize, GetInterfaceCollectionInfo(), 2); - DrawCollectionButton(buttonSize, GetPlayerCollectionInfo(), 3); - DrawCollectionButton(buttonSize, GetInheritedCollectionInfo(), 4); - - combo.Draw("##collectionSelector"u8, comboWidth, ColorId.SelectedCollection.Value()); - } - - tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors); - - if (!_activeCollections.CurrentCollectionInUse) - ImGuiUtil.DrawTextButton("The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg); - } - - private void DrawTemporaryCheckbox() - { - var hold = config.IncognitoModifier.IsActive(); - using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImUtf8.GlobalScale)) - { - var tint = config.DefaultTemporaryMode - ? ImGuiCol.Text.Tinted(ColorId.TemporaryModSettingsTint) - : ImGui.GetColorU32(ImGuiCol.TextDisabled); - using var color = ImRaii.PushColor(ImGuiCol.ButtonHovered, ImGui.GetColorU32(ImGuiCol.FrameBg), !hold) - .Push(ImGuiCol.ButtonActive, ImGui.GetColorU32(ImGuiCol.FrameBg), !hold) - .Push(ImGuiCol.Border, tint, config.DefaultTemporaryMode); - if (ImUtf8.IconButton(FontAwesomeIcon.Stopwatch, ""u8, default, false, tint, ImGui.GetColorU32(ImGuiCol.FrameBg)) && hold) - { - config.DefaultTemporaryMode = !config.DefaultTemporaryMode; - config.Save(); - } - } - - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, - "Toggle the temporary settings mode, where all changes you do create temporary settings first and need to be made permanent if desired."u8); - if (!hold) - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"\nHold {config.IncognitoModifier} while clicking to toggle."); - } - - private enum CollectionState - { - Empty, - Selected, - Unavailable, - Available, - } - - private CollectionState CheckCollection(ModCollection? collection, bool inheritance = false) - { - if (collection == null) - return CollectionState.Unavailable; - if (collection == ModCollection.Empty) - return CollectionState.Empty; - if (collection == _activeCollections.Current) - return inheritance ? CollectionState.Unavailable : CollectionState.Selected; - - return CollectionState.Available; - } - - private (ModCollection?, string, string, bool) GetDefaultCollectionInfo() - { - var collection = _activeCollections.Default; - return CheckCollection(collection) switch - { - CollectionState.Empty => (collection, "None", "The base collection is configured to use no mods.", true), - CollectionState.Selected => (collection, collection.Identity.Name, - "The configured base collection is already selected as the current collection.", true), - CollectionState.Available => (collection, collection.Identity.Name, - $"Select the configured base collection {collection.Identity.Name} as the current collection.", false), - _ => throw new Exception("Can not happen."), - }; - } - - private (ModCollection?, string, string, bool) GetPlayerCollectionInfo() - { - var collection = resolver.PlayerCollection(); - return CheckCollection(collection) switch - { - CollectionState.Empty => (collection, "None", "The loaded player character is configured to use no mods.", true), - CollectionState.Selected => (collection, collection.Identity.Name, - "The collection configured to apply to the loaded player character is already selected as the current collection.", true), - CollectionState.Available => (collection, collection.Identity.Name, - $"Select the collection {collection.Identity.Name} that applies to the loaded player character as the current collection.", - false), - _ => throw new Exception("Can not happen."), - }; - } - - private (ModCollection?, string, string, bool) GetInterfaceCollectionInfo() - { - var collection = _activeCollections.Interface; - return CheckCollection(collection) switch - { - CollectionState.Empty => (collection, "None", "The interface collection is configured to use no mods.", true), - CollectionState.Selected => (collection, collection.Identity.Name, - "The configured interface collection is already selected as the current collection.", true), - CollectionState.Available => (collection, collection.Identity.Name, - $"Select the configured interface collection {collection.Identity.Name} as the current collection.", false), - _ => throw new Exception("Can not happen."), - }; - } - - private (ModCollection?, string, string, bool) GetInheritedCollectionInfo() - { - var collection = selection.Mod == null ? null : selection.Collection; - return CheckCollection(collection, true) switch - { - CollectionState.Unavailable => (null, "Not Inherited", - "The settings of the selected mod are not inherited from another collection.", true), - CollectionState.Available => (collection, collection!.Identity.Name, - $"Select the collection {collection!.Identity.Name} from which the selected mod inherits its settings as the current collection.", - false), - _ => throw new Exception("Can not happen."), - }; - } - - private void DrawCollectionButton(Vector2 buttonWidth, (ModCollection?, string, string, bool) tuple, int id) - { - var (collection, name, tooltip, disabled) = tuple; - using var _ = ImRaii.PushId(id); - if (ImGuiUtil.DrawDisabledButton(name, buttonWidth, tooltip, disabled)) - _activeCollections.SetCollection(collection!, CollectionType.Current); - Im.Line.Same(); - } -} +using Dalamud.Interface; +using ImSharp; +using Luna; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.Interop.PathResolving; +using Penumbra.Mods; +using Penumbra.UI.CollectionTab; +using CollectionTuple = + ImSharp.RefTuple, + ImSharp.Utf8StringHandler, bool>; + +namespace Penumbra.UI.Classes; + +public class CollectionSelectHeader( + CollectionManager collectionManager, + TutorialService tutorial, + ModSelection selection, + CollectionResolver resolver, + Configuration config, + CollectionCombo combo) + : IUiService +{ + private readonly ActiveCollections _activeCollections = collectionManager.Active; + private static readonly AwesomeIcon Icon = FontAwesomeIcon.Stopwatch; + + /// Draw the header line that can quick switch between collections. + public void Draw(bool spacing) + { + using var style = ImStyleSingle.FrameRounding.Push(0) + .Push(ImStyleDouble.ItemSpacing, new Vector2(0, spacing ? Im.Style.ItemSpacing.Y : 0)); + DrawTemporaryCheckbox(); + Im.Line.Same(); + var comboWidth = Im.ContentRegion.Available.X / 4f; + var buttonSize = new Vector2(comboWidth * 3f / 4f, 0f); + using (var _ = Im.Group()) + { + DrawCollectionButton(buttonSize, GetDefaultCollectionInfo(), 1); + DrawCollectionButton(buttonSize, GetInterfaceCollectionInfo(), 2); + DrawCollectionButton(buttonSize, GetPlayerCollectionInfo(), 3); + DrawCollectionButton(buttonSize, GetInheritedCollectionInfo(), 4); + + combo.Draw("##collectionSelector"u8, comboWidth, ColorId.SelectedCollection.Value()); + } + + tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors); + + if (!_activeCollections.CurrentCollectionInUse) + ImEx.TextFramed("The currently selected collection is not used in any way."u8, -Vector2.UnitX, Colors.PressEnterWarningBg); + } + + private void DrawTemporaryCheckbox() + { + var hold = config.IncognitoModifier.IsActive(); + var tint = config.DefaultTemporaryMode + ? Rgba32.TintColor(Im.Style[ImGuiColor.Text], ColorId.TemporaryModSettingsTint.Value().ToVector()) + : Im.Style[ImGuiColor.TextDisabled]; + var frameBg = Im.Style[ImGuiColor.FrameBackground]; + + using (ImStyleBorder.Frame.Push(tint) + .Push(ImGuiColor.ButtonHovered, frameBg, !hold) + .Push(ImGuiColor.ButtonActive, frameBg, !hold)) + { + if (ImEx.Icon.Button(Icon, buttonColor: frameBg, textColor: tint) && hold) + { + config.DefaultTemporaryMode = !config.DefaultTemporaryMode; + config.Save(); + } + } + + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, + "Toggle the temporary settings mode, where all changes you do create temporary settings first and need to be made permanent if desired."u8); + if (!hold) + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"\nHold {config.IncognitoModifier} while clicking to toggle."); + } + + private enum CollectionState + { + Empty, + Selected, + Unavailable, + Available, + } + + private CollectionState CheckCollection(ModCollection? collection, bool inheritance = false) + { + if (collection is null) + return CollectionState.Unavailable; + if (collection == ModCollection.Empty) + return CollectionState.Empty; + if (collection == _activeCollections.Current) + return inheritance ? CollectionState.Unavailable : CollectionState.Selected; + + return CollectionState.Available; + } + + private CollectionTuple GetDefaultCollectionInfo() + { + var collection = _activeCollections.Default; + return CheckCollection(collection) switch + { + CollectionState.Empty => new CollectionTuple(collection, "None"u8, "The base collection is configured to use no mods."u8, true), + CollectionState.Selected => new CollectionTuple(collection, collection.Identity.Name, + "The configured base collection is already selected as the current collection."u8, true), + CollectionState.Available => new CollectionTuple(collection, collection.Identity.Name, + $"Select the configured base collection {collection.Identity.Name} as the current collection.", false), + _ => throw new Exception("Can not happen."), + }; + } + + private CollectionTuple GetPlayerCollectionInfo() + { + var collection = resolver.PlayerCollection(); + return CheckCollection(collection) switch + { + CollectionState.Empty => new CollectionTuple(collection, "None"u8, "The loaded player character is configured to use no mods."u8, + true), + CollectionState.Selected => new CollectionTuple(collection, collection.Identity.Name, + "The collection configured to apply to the loaded player character is already selected as the current collection."u8, true), + CollectionState.Available => new CollectionTuple(collection, collection.Identity.Name, + $"Select the collection {collection.Identity.Name} that applies to the loaded player character as the current collection.", + false), + _ => throw new Exception("Can not happen."), + }; + } + + private CollectionTuple GetInterfaceCollectionInfo() + { + var collection = _activeCollections.Interface; + return CheckCollection(collection) switch + { + CollectionState.Empty => new CollectionTuple(collection, "None"u8, "The interface collection is configured to use no mods."u8, + true), + CollectionState.Selected => new CollectionTuple(collection, collection.Identity.Name, + "The configured interface collection is already selected as the current collection."u8, true), + CollectionState.Available => new CollectionTuple(collection, collection.Identity.Name, + $"Select the configured interface collection {collection.Identity.Name} as the current collection.", false), + _ => throw new Exception("Can not happen."), + }; + } + + private CollectionTuple GetInheritedCollectionInfo() + { + var collection = selection.Mod is null ? null : selection.Collection; + return CheckCollection(collection, true) switch + { + CollectionState.Unavailable => new CollectionTuple(null, "Not Inherited"u8, + "The settings of the selected mod are not inherited from another collection."u8, true), + CollectionState.Available => new CollectionTuple(collection, collection!.Identity.Name, + $"Select the collection {collection.Identity.Name} from which the selected mod inherits its settings as the current collection.", + false), + _ => throw new Exception("Can not happen."), + }; + } + + private void DrawCollectionButton(Vector2 buttonWidth, in CollectionTuple tuple, int id) + { + var (collection, name, tooltip, disabled) = tuple; + using var _ = Im.Id.Push(id); + if (ImEx.Button(name, buttonWidth, tooltip, disabled)) + _activeCollections.SetCollection(collection!, CollectionType.Current); + Im.Line.Same(); + } +} diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 90ef0591..16b3c4c0 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -1,4 +1,4 @@ -using Dalamud.Bindings.ImGui; +using ImSharp; using OtterGui.Custom; namespace Penumbra.UI.Classes; @@ -53,30 +53,6 @@ public static class Colors public const uint ReniColorHovered = CustomGui.ReniColorHovered; public const uint ReniColorActive = CustomGui.ReniColorActive; - public static uint Tinted(this ColorId color, ColorId tint) - { - var tintValue = ImGui.ColorConvertU32ToFloat4(tint.Value()); - var value = ImGui.ColorConvertU32ToFloat4(color.Value()); - return ImGui.ColorConvertFloat4ToU32(TintColor(value, tintValue)); - } - - public static unsafe uint Tinted(this ImGuiCol color, ColorId tint) - { - var tintValue = ImGui.ColorConvertU32ToFloat4(tint.Value()); - ref var value = ref *ImGui.GetStyleColorVec4(color); - return ImGui.ColorConvertFloat4ToU32(TintColor(value, tintValue)); - } - - private static unsafe Vector4 TintColor(in Vector4 color, in Vector4 tint) - { - var negAlpha = 1 - tint.W; - var newAlpha = negAlpha * color.W + tint.W; - var newR = (negAlpha * color.W * color.X + tint.W * tint.X) / newAlpha; - var newG = (negAlpha * color.W * color.Y + tint.W * tint.Y) / newAlpha; - var newB = (negAlpha * color.W * color.Z + tint.W * tint.Z) / newAlpha; - return new Vector4(newR, newG, newB, newAlpha); - } - public static (uint DefaultColor, string Name, string Description) Data(this ColorId color) => color switch { @@ -116,10 +92,10 @@ public static class Colors // @formatter:on }; - private static IReadOnlyDictionary _colors = new Dictionary(); + private static Dictionary _colors = []; /// Obtain the configured value for a color. - public static uint Value(this ColorId color) + public static Rgba32 Value(this ColorId color) => _colors.TryGetValue(color, out var value) ? value : color.Data().DefaultColor; /// Set the configurable colors dictionary to a value. diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index 1b65fa0c..df0a46be 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -1,758 +1,758 @@ -using Dalamud.Game.ClientState.Objects; -using Dalamud.Interface; -using Dalamud.Interface.Components; -using Dalamud.Interface.GameFonts; -using Dalamud.Interface.ImGuiNotification; -using Dalamud.Interface.ManagedFontAtlas; -using Dalamud.Interface.Utility; -using Dalamud.Plugin; -using Dalamud.Bindings.ImGui; -using ImSharp; -using Luna; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Text; -using Penumbra.Collections; -using Penumbra.Collections.Manager; -using Penumbra.GameData.Actors; -using Penumbra.GameData.Enums; -using Penumbra.Mods.Manager; -using Penumbra.Services; -using Penumbra.UI.Classes; - -namespace Penumbra.UI.CollectionTab; - -public sealed class CollectionPanel( - IDalamudPluginInterface pi, - CommunicatorService communicator, - CollectionManager manager, - CollectionSelector selector, - ActorManager actors, - ITargetManager targets, - ModStorage mods, - SaveService saveService, - IncognitoService incognito) - : IDisposable -{ - private readonly CollectionStorage _collections = manager.Storage; - private readonly ActiveCollections _active = manager.Active; - private readonly IndividualAssignmentUi _individualAssignmentUi = new(communicator, actors, manager); - private readonly InheritanceUi _inheritanceUi = new(manager, incognito); - private readonly IFontHandle _nameFont = pi.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); - - private static readonly IReadOnlyDictionary Buttons = CreateButtons(); - private static readonly IReadOnlyList<(CollectionType, bool, bool, string, uint)> AdvancedTree = CreateTree(); - private readonly List<(CollectionType Type, ActorIdentifier Identifier)> _inUseCache = []; - private string? _newName; - - private int _draggedIndividualAssignment = -1; - - public void Dispose() - { - _individualAssignmentUi.Dispose(); - _nameFont.Dispose(); - } - - /// Draw the panel containing beginners information and simple assignments. - public void DrawSimple() - { - Im.TextWrapped("A collection is a set of mod configurations. You can have as many collections as you desire.\n"u8 - + "The collection you are currently editing in the mod tab can be selected here and is highlighted.\n"u8); - Im.TextWrapped( - "There are functions you can assign these collections to, so different mod configurations apply for different things.\n"u8 - + "You can assign an existing collection to such a function by clicking the function or dragging the collection over."u8); - ImGui.Separator(); - - var buttonWidth = new Vector2(200 * Im.Style.GlobalScale, 2 * Im.Style.FrameHeightWithSpacing); - using var style = Im.Style.Push(ImStyleDouble.ButtonTextAlign, Vector2.Zero) - .Push(ImStyleSingle.FrameBorderThickness, Im.Style.GlobalScale); - DrawSimpleCollectionButton(CollectionType.Default, buttonWidth); - DrawSimpleCollectionButton(CollectionType.Interface, buttonWidth); - DrawSimpleCollectionButton(CollectionType.Yourself, buttonWidth); - DrawSimpleCollectionButton(CollectionType.MalePlayerCharacter, buttonWidth); - DrawSimpleCollectionButton(CollectionType.FemalePlayerCharacter, buttonWidth); - DrawSimpleCollectionButton(CollectionType.MaleNonPlayerCharacter, buttonWidth); - DrawSimpleCollectionButton(CollectionType.FemaleNonPlayerCharacter, buttonWidth); +using Dalamud.Game.ClientState.Objects; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.Utility; +using Dalamud.Plugin; +using Dalamud.Bindings.ImGui; +using ImSharp; +using Luna; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; +using Penumbra.Mods.Manager; +using Penumbra.Services; +using Penumbra.UI.Classes; - ImGuiUtil.DrawColoredText(("Individual ", ColorId.NewMod.Value()), - ("Assignments take precedence before anything else and only apply to one specific character or monster.", 0)); - ImGui.Dummy(Vector2.UnitX); - - var specialWidth = buttonWidth with { X = 275 * ImGuiHelpers.GlobalScale }; - DrawCurrentCharacter(specialWidth); - Im.Line.Same(); - DrawCurrentTarget(specialWidth); - DrawIndividualCollections(buttonWidth); - - var first = true; - - Button(CollectionType.NonPlayerChild); - Button(CollectionType.NonPlayerElderly); - foreach (var race in Enum.GetValues().Skip(1)) - { - Button(CollectionTypeExtensions.FromParts(race, Gender.Male, false)); - Button(CollectionTypeExtensions.FromParts(race, Gender.Female, false)); - Button(CollectionTypeExtensions.FromParts(race, Gender.Male, true)); - Button(CollectionTypeExtensions.FromParts(race, Gender.Female, true)); - } - - return; - - void Button(CollectionType type) - { - var (name, border) = Buttons[type]; - var collection = _active.ByType(type); - if (collection == null) - return; - - if (first) - { - ImGui.Separator(); - ImGui.TextUnformatted("Currently Active Advanced Assignments"); - first = false; - } - - DrawButton(name, type, buttonWidth, border, ActorIdentifier.Invalid, 's', collection); - Im.Line.Same(); - if (ImGui.GetContentRegionAvail().X < buttonWidth.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X) - ImGui.NewLine(); - } - } - - /// Draw the panel containing new and existing individual assignments. - public void DrawIndividualPanel() - { - using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero) - .Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale); - var width = new Vector2(300 * ImGuiHelpers.GlobalScale, 2 * ImGui.GetTextLineHeightWithSpacing()); - - ImGui.Dummy(Vector2.One); - DrawCurrentCharacter(width); - Im.Line.Same(); - DrawCurrentTarget(width); - ImGui.Separator(); - ImGui.Dummy(Vector2.One); - style.Pop(); - _individualAssignmentUi.DrawWorldCombo(width.X / 2); - Im.Line.Same(); - _individualAssignmentUi.DrawNewPlayerCollection(width.X); - - _individualAssignmentUi.DrawObjectKindCombo(width.X / 2); - Im.Line.Same(); - _individualAssignmentUi.DrawNewNpcCollection(width.X); - Im.Line.Same(); - ImGuiComponents.HelpMarker( - "Battle- and Event NPCs may apply to more than one ID if they share the same name. This is language dependent. If you change your clients language, verify that your collections are still correctly assigned."); - ImGui.Dummy(Vector2.One); - ImGui.Separator(); - style.Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale); - - DrawNewPlayer(width); - Im.Line.Same(); - ImGuiUtil.TextWrapped("Also check General Settings for UI characters and inheritance through ownership."); - ImGui.Separator(); - - DrawNewRetainer(width); - Im.Line.Same(); - ImGuiUtil.TextWrapped("Bell Retainers apply to Mannequins, but not to outdoor retainers, since those only carry their owners name."); - ImGui.Separator(); - - DrawNewNpc(width); - Im.Line.Same(); - ImGuiUtil.TextWrapped("Some NPCs are available as Battle - and Event NPCs and need to be setup for both if desired."); - ImGui.Separator(); - - DrawNewOwned(width); - Im.Line.Same(); - ImGuiUtil.TextWrapped("Owned NPCs take precedence before unowned NPCs of the same type."); - ImGui.Separator(); - - DrawIndividualCollections(width with { X = 200 * ImGuiHelpers.GlobalScale }); - } - - /// Draw the panel containing all special group assignments. - public void DrawGroupPanel() - { - ImGui.Dummy(Vector2.One); - using var table = ImRaii.Table("##advanced", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); - if (!table) - return; - - using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero) - .Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale); - - var buttonWidth = new Vector2(150 * ImGuiHelpers.GlobalScale, 2 * ImGui.GetTextLineHeightWithSpacing()); - var dummy = new Vector2(1, 0); - - foreach (var (type, pre, post, name, border) in AdvancedTree) - { - ImGui.TableNextColumn(); - if (type is CollectionType.Inactive) - continue; - - if (pre) - ImGui.Dummy(dummy); - DrawAssignmentButton(type, buttonWidth, name, border); - if (post) - ImGui.Dummy(dummy); - } - } - - /// Draw the collection detail panel with inheritance, visible mod settings and statistics. - public void DrawDetailsPanel() - { - var collection = _active.Current; - DrawCollectionName(collection); - DrawStatistics(collection); - DrawCollectionData(collection); - _inheritanceUi.Draw(); - ImGui.Separator(); - DrawInactiveSettingsList(collection); - DrawSettingsList(collection); - } - - private void DrawCollectionData(ModCollection collection) - { - ImGui.Dummy(Vector2.Zero); - ImGui.BeginGroup(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("Name"); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("Identifier"); - ImGui.EndGroup(); - Im.Line.Same(); - ImGui.BeginGroup(); - var width = ImGui.GetContentRegionAvail().X; - using (ImRaii.Disabled(_collections.DefaultNamed == collection)) - { - using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); - var name = _newName ?? collection.Identity.Name; - ImGui.SetNextItemWidth(width); - if (ImGui.InputText("##name", ref name, 128)) - _newName = name; - if (ImGui.IsItemDeactivatedAfterEdit() && _newName != null && _newName != collection.Identity.Name) - { - collection.Identity.Name = _newName; - saveService.QueueSave(new ModCollectionSave(mods, collection)); - selector.RestoreCollections(); - _newName = null; - } - else if (ImGui.IsItemDeactivated()) - { - _newName = null; - } - } - if (_collections.DefaultNamed == collection) - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "The Default collection can not be renamed."u8); - - var identifier = collection.Identity.Identifier; - var fileName = saveService.FileNames.CollectionFile(collection); - using (ImRaii.PushFont(UiBuilder.MonoFont)) - { - if (ImGui.Button(collection.Identity.Identifier, new Vector2(width, 0))) - try - { - Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); - } - catch (Exception ex) - { - Penumbra.Messager.NotificationMessage(ex, $"Could not open file {fileName}.", $"Could not open file {fileName}", - NotificationType.Warning); - } - } - - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - ImGui.SetClipboardText(identifier); - - ImGuiUtil.HoverTooltip( - $"Open the file\n\t{fileName}\ncontaining this design in the .json-editor of your choice.\n\nRight-Click to copy identifier to clipboard."); - - ImGui.EndGroup(); - ImGui.Dummy(Vector2.Zero); - ImGui.Separator(); - ImGui.Dummy(Vector2.Zero); - } - - private void DrawContext(bool open, ModCollection? collection, CollectionType type, ActorIdentifier identifier, string text, char suffix) - { - var label = $"{type}{text}{suffix}"; - if (open) - ImGui.OpenPopup(label); - - using var context = ImRaii.Popup(label); - if (!context) - return; - - using (var color = ImRaii.PushColor(ImGuiCol.Text, Colors.DiscordColor)) - { - if (ImGui.MenuItem("Use no mods.")) - _active.SetCollection(ModCollection.Empty, type, _active.Individuals.GetGroup(identifier)); - } - - if (collection != null && type.CanBeRemoved()) - { - using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); - if (ImGui.MenuItem("Remove this assignment.")) - _active.SetCollection(null, type, _active.Individuals.GetGroup(identifier)); - } - - foreach (var coll in _collections.OrderBy(c => c.Identity.Name)) - { - if (coll != collection && ImGui.MenuItem($"Use {coll.Identity.Name}.")) - _active.SetCollection(coll, type, _active.Individuals.GetGroup(identifier)); - } - } - - private bool DrawButton(string text, CollectionType type, Vector2 width, uint borderColor, ActorIdentifier id, char suffix, - ModCollection? collection = null) - { - using var group = ImRaii.Group(); - var invalid = type == CollectionType.Individual && !id.IsValid; - var redundancy = _active.RedundancyCheck(type, id); - collection ??= _active.ByType(type, id); - using var color = ImRaii.PushColor(ImGuiCol.Button, - collection == null - ? ColorId.NoAssignment.Value() - : redundancy.Length > 0 - ? ColorId.RedundantAssignment.Value() - : collection == _active.Current - ? ColorId.SelectedCollection.Value() - : collection == ModCollection.Empty - ? ColorId.NoModsAssignment.Value() - : ImGui.GetColorU32(ImGuiCol.Button), !invalid) - .Push(ImGuiCol.Border, borderColor == 0 ? ImGui.GetColorU32(ImGuiCol.TextDisabled) : borderColor); - using var disabled = ImRaii.Disabled(invalid); - var button = ImGui.Button(text, width) || ImGui.IsItemClicked(ImGuiMouseButton.Right); - var hovered = redundancy.Length > 0 && ImGui.IsItemHovered(); - DrawIndividualDragSource(text, id); - DrawIndividualDragTarget(id); - if (!invalid) - { - selector.DragTargetAssignment(type, id); - var name = Name(collection); - var size = ImGui.CalcTextSize(name); - var textPos = ImGui.GetItemRectMax() - size - ImGui.GetStyle().FramePadding; - ImGui.GetWindowDrawList().AddText(textPos, ImGui.GetColorU32(ImGuiCol.Text), name); - DrawContext(button, collection, type, id, text, suffix); - } - - if (hovered) - ImGui.SetTooltip(redundancy); - - return button; - } - - private void DrawIndividualDragSource(string text, ActorIdentifier id) - { - if (!id.IsValid) - return; - - using var source = ImRaii.DragDropSource(); - if (!source) - return; - - ImGui.SetDragDropPayload("DragIndividual", null, 0); - ImGui.TextUnformatted($"Re-ordering {text}..."); - _draggedIndividualAssignment = _active.Individuals.Index(id); - } - - private void DrawIndividualDragTarget(ActorIdentifier id) - { - if (!id.IsValid) - return; - - using var target = ImRaii.DragDropTarget(); - if (!target || !ImGuiUtil.IsDropping("DragIndividual")) - return; - - var currentIdx = _active.Individuals.Index(id); - if (_draggedIndividualAssignment != -1 && currentIdx != -1) - _active.MoveIndividualCollection(_draggedIndividualAssignment, currentIdx); - _draggedIndividualAssignment = -1; - } - - private void DrawSimpleCollectionButton(CollectionType type, Vector2 width) - { - DrawButton(type.ToName(), type, width, 0, ActorIdentifier.Invalid, 's'); - Im.Line.Same(); - using (var group = ImRaii.Group()) - { - ImGuiUtil.TextWrapped(type.ToDescription()); - switch (type) - { - case CollectionType.Default: ImGui.TextUnformatted("Overruled by any other Assignment."); break; - case CollectionType.Yourself: - ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Individual ", ColorId.NewMod.Value()), ("Assignments.", 0)); - break; - case CollectionType.MalePlayerCharacter: - ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Male Racial Player", Colors.DiscordColor), (", ", 0), - ("Your Character", ColorId.HandledConflictMod.Value()), (", or ", 0), - ("Individual ", ColorId.NewMod.Value()), ("Assignments.", 0)); - break; - case CollectionType.FemalePlayerCharacter: - ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Female Racial Player", Colors.ReniColorActive), (", ", 0), - ("Your Character", ColorId.HandledConflictMod.Value()), (", or ", 0), - ("Individual ", ColorId.NewMod.Value()), ("Assignments.", 0)); - break; - case CollectionType.MaleNonPlayerCharacter: - ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Male Racial NPC", Colors.DiscordColor), (", ", 0), - ("Children", ColorId.FolderLine.Value()), (", ", 0), ("Elderly", Colors.MetaInfoText), (", or ", 0), - ("Individual ", ColorId.NewMod.Value()), ("Assignments.", 0)); - break; - case CollectionType.FemaleNonPlayerCharacter: - ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Female Racial NPC", Colors.ReniColorActive), (", ", 0), - ("Children", ColorId.FolderLine.Value()), (", ", 0), ("Elderly", Colors.MetaInfoText), (", or ", 0), - ("Individual ", ColorId.NewMod.Value()), ("Assignments.", 0)); - break; - } - } - - ImGui.Separator(); - } - - private void DrawAssignmentButton(CollectionType type, Vector2 width, string name, uint color) - => DrawButton(name, type, width, color, ActorIdentifier.Invalid, 's', _active.ByType(type)); - - /// Respect incognito mode for names of identifiers. - private string Name(ActorIdentifier id, string? name) - => incognito.IncognitoMode && id.Type is IdentifierType.Player or IdentifierType.Owned - ? id.Incognito(name) - : name ?? id.ToString(); - - /// Respect incognito mode for names of collections. - private string Name(ModCollection? collection) - => collection == null ? "Unassigned" : - collection == ModCollection.Empty ? "Use No Mods" : - incognito.IncognitoMode ? collection.Identity.AnonymizedName : collection.Identity.Name; - - private void DrawIndividualButton(string intro, Vector2 width, string tooltip, char suffix, params ActorIdentifier[] identifiers) - { - if (identifiers.Length > 0 && identifiers[0].IsValid) - { - DrawButton($"{intro} ({Name(identifiers[0], null)})", CollectionType.Individual, width, 0, identifiers[0], suffix); - } - else - { - if (tooltip.Length == 0 && identifiers.Length > 0) - tooltip = $"The current target {identifiers[0].PlayerName} is not valid for an assignment."; - DrawButton($"{intro} (Unavailable)", CollectionType.Individual, width, 0, ActorIdentifier.Invalid, suffix); - } - - ImGuiUtil.HoverTooltip(tooltip); - } - - private void DrawCurrentCharacter(Vector2 width) - => DrawIndividualButton("Current Character", width, string.Empty, 'c', actors.GetCurrentPlayer()); - - private void DrawCurrentTarget(Vector2 width) - => DrawIndividualButton("Current Target", width, string.Empty, 't', - actors.FromObject(targets.Target, false, true, true)); - - private void DrawNewPlayer(Vector2 width) - => DrawIndividualButton("New Player", width, _individualAssignmentUi.PlayerTooltip, 'p', - _individualAssignmentUi.PlayerIdentifiers.FirstOrDefault()); - - private void DrawNewRetainer(Vector2 width) - => DrawIndividualButton("New Bell Retainer", width, _individualAssignmentUi.RetainerTooltip, 'r', - _individualAssignmentUi.RetainerIdentifiers.FirstOrDefault()); - - private void DrawNewNpc(Vector2 width) - => DrawIndividualButton("New NPC", width, _individualAssignmentUi.NpcTooltip, 'n', - _individualAssignmentUi.NpcIdentifiers.FirstOrDefault()); - - private void DrawNewOwned(Vector2 width) - => DrawIndividualButton("New Owned NPC", width, _individualAssignmentUi.OwnedTooltip, 'o', - _individualAssignmentUi.OwnedIdentifiers.FirstOrDefault()); - - private void DrawIndividualCollections(Vector2 width) - { - for (var i = 0; i < _active.Individuals.Count; ++i) - { - var (name, ids, coll) = _active.Individuals.Assignments[i]; - DrawButton(Name(ids[0], name), CollectionType.Individual, width, 0, ids[0], 'i', coll); - - Im.Line.Same(); - if (ImGui.GetContentRegionAvail().X < width.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X - && i < _active.Individuals.Count - 1) - ImGui.NewLine(); - } - - if (_active.Individuals.Count > 0) - ImGui.NewLine(); - } - - private void DrawCollectionName(ModCollection collection) - { - ImGui.Dummy(Vector2.One); - using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.MetaInfoText); - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * UiHelpers.Scale); - using var f = _nameFont.Push(); - var name = Name(collection); - var size = ImGui.CalcTextSize(name).X; - var pos = ImGui.GetContentRegionAvail().X - size + ImGui.GetStyle().FramePadding.X * 2; - if (pos > 0) - ImGui.SetCursorPosX(pos / 2); - ImGuiUtil.DrawTextButton(name, Vector2.Zero, 0); - ImGui.Dummy(Vector2.One); - } - - private void DrawStatistics(ModCollection collection) - { - GatherInUse(collection); - ImGui.Separator(); - - var buttonHeight = 2 * ImGui.GetTextLineHeightWithSpacing(); - if (_inUseCache.Count == 0 && collection.Inheritance.DirectlyInheritedBy.Count == 0) - { - ImGui.Dummy(Vector2.One); - using var f = _nameFont.Push(); - ImGuiUtil.DrawTextButton("Collection is not used.", new Vector2(ImGui.GetContentRegionAvail().X, buttonHeight), - Colors.PressEnterWarningBg); - ImGui.Dummy(Vector2.One); - ImGui.Separator(); - } - else - { - var buttonWidth = new Vector2(175 * ImGuiHelpers.GlobalScale, buttonHeight); - DrawInUseStatistics(collection, buttonWidth); - DrawInheritanceStatistics(collection, buttonWidth); - } - } - - private void GatherInUse(ModCollection collection) - { - _inUseCache.Clear(); - foreach (var special in CollectionTypeExtensions.Special.Select(t => t.Item1) - .Prepend(CollectionType.Default) - .Prepend(CollectionType.Interface) - .Where(t => _active.ByType(t) == collection)) - _inUseCache.Add((special, ActorIdentifier.Invalid)); - - foreach (var (_, id, coll) in _active.Individuals.Assignments.Where(t - => t.Collection == collection && t.Identifiers.FirstOrDefault().IsValid)) - _inUseCache.Add((CollectionType.Individual, id[0])); - } - - private void DrawInUseStatistics(ModCollection collection, Vector2 buttonWidth) - { - if (_inUseCache.Count <= 0) - return; - - using (var _ = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero)) - { - ImGuiUtil.DrawTextButton("In Use By", ImGui.GetContentRegionAvail() with { Y = 0 }, 0); - } - - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale) - .Push(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero); - - foreach (var (idx, (type, id)) in _inUseCache.Index()) - { - var name = type == CollectionType.Individual ? Name(id, null) : Buttons[type].Name; - var color = Buttons.TryGetValue(type, out var p) ? p.Border : 0; - DrawButton(name, type, buttonWidth, color, id, 's', collection); - Im.Line.Same(); - if (ImGui.GetContentRegionAvail().X < buttonWidth.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X - && idx != _inUseCache.Count - 1) - ImGui.NewLine(); - } - - ImGui.NewLine(); - ImGui.Dummy(Vector2.One); - ImGui.Separator(); - } - - private void DrawInheritanceStatistics(ModCollection collection, Vector2 buttonWidth) - { - if (collection.Inheritance.DirectlyInheritedBy.Count <= 0) - return; - - using (var _ = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero)) - { - ImGuiUtil.DrawTextButton("Inherited by", ImGui.GetContentRegionAvail() with { Y = 0 }, 0); - } - - using var f = _nameFont.Push(); - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); - using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.MetaInfoText); - ImGuiUtil.DrawTextButton(Name(collection.Inheritance.DirectlyInheritedBy[0]), Vector2.Zero, 0); - var constOffset = (ImGui.GetStyle().FramePadding.X + ImGuiHelpers.GlobalScale) * 2 - + ImGui.GetStyle().ItemSpacing.X - + ImGui.GetStyle().WindowPadding.X; - foreach (var parent in collection.Inheritance.DirectlyInheritedBy.Skip(1)) - { - var name = Name(parent); - var size = ImGui.CalcTextSize(name).X; - Im.Line.Same(); - if (constOffset + size >= ImGui.GetContentRegionAvail().X) - ImGui.NewLine(); - ImGuiUtil.DrawTextButton(name, Vector2.Zero, 0); - } - - ImGui.Dummy(Vector2.One); - ImGui.Separator(); - } - - private void DrawSettingsList(ModCollection collection) - { - ImGui.Dummy(Vector2.One); - var size = new Vector2(ImGui.GetContentRegionAvail().X, 10 * ImGui.GetFrameHeightWithSpacing()); - using var table = ImRaii.Table("##activeSettings", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, size); - if (!table) - return; - - ImGui.TableSetupScrollFreeze(0, 1); - ImGui.TableSetupColumn("Mod Name", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Inherited From", ImGuiTableColumnFlags.WidthFixed, 5f * ImGui.GetFrameHeight()); - ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, 1.75f * ImGui.GetFrameHeight()); - ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthFixed, 2.5f * ImGui.GetFrameHeight()); - ImGui.TableHeadersRow(); - foreach (var (mod, (settings, parent)) in mods.Select(m => (m, collection.GetInheritedSettings(m.Index))) - .Where(t => t.Item2.Settings != null) - .OrderBy(t => t.m.Name)) - { - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(mod.Name); - ImGui.TableNextColumn(); - if (parent != collection) - ImGui.TextUnformatted(Name(parent)); - ImGui.TableNextColumn(); - var enabled = settings!.Enabled; - using (var dis = ImRaii.Disabled()) - { - ImGui.Checkbox("##check", ref enabled); - } - - ImGui.TableNextColumn(); - ImGuiUtil.RightAlign(settings.Priority.ToString(), ImGui.GetStyle().WindowPadding.X); - } - } - - private void DrawInactiveSettingsList(ModCollection collection) - { - if (collection.Settings.Unused.Count == 0) - return; - - ImGui.Dummy(Vector2.One); - var text = collection.Settings.Unused.Count > 1 - ? $"Clear all {collection.Settings.Unused.Count} unused settings from deleted mods." - : "Clear the currently unused setting from a deleted mods."; - if (ImGui.Button(text, new Vector2(ImGui.GetContentRegionAvail().X, 0))) - _collections.CleanUnavailableSettings(collection); - - ImGui.Dummy(Vector2.One); - - var size = new Vector2(ImGui.GetContentRegionAvail().X, - Math.Min(10, collection.Settings.Unused.Count + 1) * ImGui.GetFrameHeightWithSpacing()); - using var table = ImRaii.Table("##inactiveSettings", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, size); - if (!table) - return; - - ImGui.TableSetupScrollFreeze(0, 1); - ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); - ImGui.TableSetupColumn("Unused Mod Identifier", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, 1.75f * ImGui.GetFrameHeight()); - ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthFixed, 2.5f * ImGui.GetFrameHeight()); - ImGui.TableHeadersRow(); - string? delete = null; - foreach (var (name, settings) in collection.Settings.Unused.OrderBy(n => n.Key)) - { - using var id = ImRaii.PushId(name); - ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, - "Delete this unused setting.", false, true)) - delete = name; - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(name); - ImGui.TableNextColumn(); - var enabled = settings.Enabled; - using (var dis = ImRaii.Disabled()) - { - ImGui.Checkbox("##check", ref enabled); - } - - ImGui.TableNextColumn(); - ImGuiUtil.RightAlign(settings.Priority.ToString(), ImGui.GetStyle().WindowPadding.X); - } - - _collections.CleanUnavailableSetting(collection, delete); - ImGui.Separator(); - } - - /// Create names and border colors for special assignments. - private static IReadOnlyDictionary CreateButtons() - { - var ret = Enum.GetValues().ToDictionary(t => t, t => (t.ToName(), 0u)); - - foreach (var race in Enum.GetValues().Skip(1)) - { - var color = race switch - { - SubRace.Midlander => 0xAA5C9FE4u, - SubRace.Highlander => 0xAA5C9FE4u, - SubRace.Wildwood => 0xAA5C9F49u, - SubRace.Duskwight => 0xAA5C9F49u, - SubRace.Plainsfolk => 0xAAEF8CB6u, - SubRace.Dunesfolk => 0xAAEF8CB6u, - SubRace.SeekerOfTheSun => 0xAA8CEFECu, - SubRace.KeeperOfTheMoon => 0xAA8CEFECu, - SubRace.Seawolf => 0xAAEFE68Cu, - SubRace.Hellsguard => 0xAAEFE68Cu, - SubRace.Raen => 0xAAB5EF8Cu, - SubRace.Xaela => 0xAAB5EF8Cu, - SubRace.Helion => 0xAAFFFFFFu, - SubRace.Lost => 0xAAFFFFFFu, - SubRace.Rava => 0xAA607FA7u, - SubRace.Veena => 0xAA607FA7u, - _ => 0u, - }; - - ret[CollectionTypeExtensions.FromParts(race, Gender.Male, false)] = ($"♂ {race.ToShortName()}", color); - ret[CollectionTypeExtensions.FromParts(race, Gender.Female, false)] = ($"♀ {race.ToShortName()}", color); - ret[CollectionTypeExtensions.FromParts(race, Gender.Male, true)] = ($"♂ {race.ToShortName()} (NPC)", color); - ret[CollectionTypeExtensions.FromParts(race, Gender.Female, true)] = ($"♀ {race.ToShortName()} (NPC)", color); - } - - ret[CollectionType.MalePlayerCharacter] = ("♂ Player", 0); - ret[CollectionType.FemalePlayerCharacter] = ("♀ Player", 0); - ret[CollectionType.MaleNonPlayerCharacter] = ("♂ NPC", 0); - ret[CollectionType.FemaleNonPlayerCharacter] = ("♀ NPC", 0); - return ret; - } - - /// Create the special assignment tree in order and with free spaces. - private static IReadOnlyList<(CollectionType, bool, bool, string, uint)> CreateTree() - { - var ret = new List<(CollectionType, bool, bool, string, uint)>(Buttons.Count); - - void Add(CollectionType type, bool pre, bool post) - { - var (name, border) = Buttons[type]; - ret.Add((type, pre, post, name, border)); - } - - Add(CollectionType.Default, false, false); - Add(CollectionType.Interface, false, false); - Add(CollectionType.Inactive, false, false); - Add(CollectionType.Inactive, false, false); - Add(CollectionType.Yourself, false, true); - Add(CollectionType.Inactive, false, true); - Add(CollectionType.NonPlayerChild, false, true); - Add(CollectionType.NonPlayerElderly, false, true); - Add(CollectionType.MalePlayerCharacter, true, true); - Add(CollectionType.FemalePlayerCharacter, true, true); - Add(CollectionType.MaleNonPlayerCharacter, true, true); - Add(CollectionType.FemaleNonPlayerCharacter, true, true); - var pre = true; - foreach (var race in Enum.GetValues().Skip(1)) - { - Add(CollectionTypeExtensions.FromParts(race, Gender.Male, false), pre, !pre); - Add(CollectionTypeExtensions.FromParts(race, Gender.Female, false), pre, !pre); - Add(CollectionTypeExtensions.FromParts(race, Gender.Male, true), pre, !pre); - Add(CollectionTypeExtensions.FromParts(race, Gender.Female, true), pre, !pre); - pre = !pre; - } - - return ret; - } -} +namespace Penumbra.UI.CollectionTab; + +public sealed class CollectionPanel( + IDalamudPluginInterface pi, + CommunicatorService communicator, + CollectionManager manager, + CollectionSelector selector, + ActorManager actors, + ITargetManager targets, + ModStorage mods, + SaveService saveService, + IncognitoService incognito) + : IDisposable +{ + private readonly CollectionStorage _collections = manager.Storage; + private readonly ActiveCollections _active = manager.Active; + private readonly IndividualAssignmentUi _individualAssignmentUi = new(communicator, actors, manager); + private readonly InheritanceUi _inheritanceUi = new(manager, incognito); + private readonly IFontHandle _nameFont = pi.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); + + private static readonly IReadOnlyDictionary Buttons = CreateButtons(); + private static readonly IReadOnlyList<(CollectionType, bool, bool, string, uint)> AdvancedTree = CreateTree(); + private readonly List<(CollectionType Type, ActorIdentifier Identifier)> _inUseCache = []; + private string? _newName; + + private int _draggedIndividualAssignment = -1; + + public void Dispose() + { + _individualAssignmentUi.Dispose(); + _nameFont.Dispose(); + } + + /// Draw the panel containing beginners information and simple assignments. + public void DrawSimple() + { + Im.TextWrapped("A collection is a set of mod configurations. You can have as many collections as you desire.\n"u8 + + "The collection you are currently editing in the mod tab can be selected here and is highlighted.\n"u8); + Im.TextWrapped( + "There are functions you can assign these collections to, so different mod configurations apply for different things.\n"u8 + + "You can assign an existing collection to such a function by clicking the function or dragging the collection over."u8); + ImGui.Separator(); + + var buttonWidth = new Vector2(200 * Im.Style.GlobalScale, 2 * Im.Style.FrameHeightWithSpacing); + using var style = Im.Style.Push(ImStyleDouble.ButtonTextAlign, Vector2.Zero) + .Push(ImStyleSingle.FrameBorderThickness, Im.Style.GlobalScale); + DrawSimpleCollectionButton(CollectionType.Default, buttonWidth); + DrawSimpleCollectionButton(CollectionType.Interface, buttonWidth); + DrawSimpleCollectionButton(CollectionType.Yourself, buttonWidth); + DrawSimpleCollectionButton(CollectionType.MalePlayerCharacter, buttonWidth); + DrawSimpleCollectionButton(CollectionType.FemalePlayerCharacter, buttonWidth); + DrawSimpleCollectionButton(CollectionType.MaleNonPlayerCharacter, buttonWidth); + DrawSimpleCollectionButton(CollectionType.FemaleNonPlayerCharacter, buttonWidth); + + ImGuiUtil.DrawColoredText(("Individual ", ColorId.NewMod.Value().Color), + ("Assignments take precedence before anything else and only apply to one specific character or monster.", 0)); + ImGui.Dummy(Vector2.UnitX); + + var specialWidth = buttonWidth with { X = 275 * ImGuiHelpers.GlobalScale }; + DrawCurrentCharacter(specialWidth); + Im.Line.Same(); + DrawCurrentTarget(specialWidth); + DrawIndividualCollections(buttonWidth); + + var first = true; + + Button(CollectionType.NonPlayerChild); + Button(CollectionType.NonPlayerElderly); + foreach (var race in Enum.GetValues().Skip(1)) + { + Button(CollectionTypeExtensions.FromParts(race, Gender.Male, false)); + Button(CollectionTypeExtensions.FromParts(race, Gender.Female, false)); + Button(CollectionTypeExtensions.FromParts(race, Gender.Male, true)); + Button(CollectionTypeExtensions.FromParts(race, Gender.Female, true)); + } + + return; + + void Button(CollectionType type) + { + var (name, border) = Buttons[type]; + var collection = _active.ByType(type); + if (collection == null) + return; + + if (first) + { + ImGui.Separator(); + ImGui.TextUnformatted("Currently Active Advanced Assignments"); + first = false; + } + + DrawButton(name, type, buttonWidth, border, ActorIdentifier.Invalid, 's', collection); + Im.Line.Same(); + if (ImGui.GetContentRegionAvail().X < buttonWidth.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X) + ImGui.NewLine(); + } + } + + /// Draw the panel containing new and existing individual assignments. + public void DrawIndividualPanel() + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero) + .Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale); + var width = new Vector2(300 * ImGuiHelpers.GlobalScale, 2 * ImGui.GetTextLineHeightWithSpacing()); + + ImGui.Dummy(Vector2.One); + DrawCurrentCharacter(width); + Im.Line.Same(); + DrawCurrentTarget(width); + ImGui.Separator(); + ImGui.Dummy(Vector2.One); + style.Pop(); + _individualAssignmentUi.DrawWorldCombo(width.X / 2); + Im.Line.Same(); + _individualAssignmentUi.DrawNewPlayerCollection(width.X); + + _individualAssignmentUi.DrawObjectKindCombo(width.X / 2); + Im.Line.Same(); + _individualAssignmentUi.DrawNewNpcCollection(width.X); + Im.Line.Same(); + ImGuiComponents.HelpMarker( + "Battle- and Event NPCs may apply to more than one ID if they share the same name. This is language dependent. If you change your clients language, verify that your collections are still correctly assigned."); + ImGui.Dummy(Vector2.One); + ImGui.Separator(); + style.Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale); + + DrawNewPlayer(width); + Im.Line.Same(); + ImGuiUtil.TextWrapped("Also check General Settings for UI characters and inheritance through ownership."); + ImGui.Separator(); + + DrawNewRetainer(width); + Im.Line.Same(); + ImGuiUtil.TextWrapped("Bell Retainers apply to Mannequins, but not to outdoor retainers, since those only carry their owners name."); + ImGui.Separator(); + + DrawNewNpc(width); + Im.Line.Same(); + ImGuiUtil.TextWrapped("Some NPCs are available as Battle - and Event NPCs and need to be setup for both if desired."); + ImGui.Separator(); + + DrawNewOwned(width); + Im.Line.Same(); + ImGuiUtil.TextWrapped("Owned NPCs take precedence before unowned NPCs of the same type."); + ImGui.Separator(); + + DrawIndividualCollections(width with { X = 200 * ImGuiHelpers.GlobalScale }); + } + + /// Draw the panel containing all special group assignments. + public void DrawGroupPanel() + { + ImGui.Dummy(Vector2.One); + using var table = ImRaii.Table("##advanced", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + return; + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero) + .Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale); + + var buttonWidth = new Vector2(150 * ImGuiHelpers.GlobalScale, 2 * ImGui.GetTextLineHeightWithSpacing()); + var dummy = new Vector2(1, 0); + + foreach (var (type, pre, post, name, border) in AdvancedTree) + { + ImGui.TableNextColumn(); + if (type is CollectionType.Inactive) + continue; + + if (pre) + ImGui.Dummy(dummy); + DrawAssignmentButton(type, buttonWidth, name, border); + if (post) + ImGui.Dummy(dummy); + } + } + + /// Draw the collection detail panel with inheritance, visible mod settings and statistics. + public void DrawDetailsPanel() + { + var collection = _active.Current; + DrawCollectionName(collection); + DrawStatistics(collection); + DrawCollectionData(collection); + _inheritanceUi.Draw(); + ImGui.Separator(); + DrawInactiveSettingsList(collection); + DrawSettingsList(collection); + } + + private void DrawCollectionData(ModCollection collection) + { + ImGui.Dummy(Vector2.Zero); + ImGui.BeginGroup(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Name"); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Identifier"); + ImGui.EndGroup(); + Im.Line.Same(); + ImGui.BeginGroup(); + var width = ImGui.GetContentRegionAvail().X; + using (ImRaii.Disabled(_collections.DefaultNamed == collection)) + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); + var name = _newName ?? collection.Identity.Name; + ImGui.SetNextItemWidth(width); + if (ImGui.InputText("##name", ref name, 128)) + _newName = name; + if (ImGui.IsItemDeactivatedAfterEdit() && _newName != null && _newName != collection.Identity.Name) + { + collection.Identity.Name = _newName; + saveService.QueueSave(new ModCollectionSave(mods, collection)); + selector.RestoreCollections(); + _newName = null; + } + else if (ImGui.IsItemDeactivated()) + { + _newName = null; + } + } + if (_collections.DefaultNamed == collection) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "The Default collection can not be renamed."u8); + + var identifier = collection.Identity.Identifier; + var fileName = saveService.FileNames.CollectionFile(collection); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + if (ImGui.Button(collection.Identity.Identifier, new Vector2(width, 0))) + try + { + Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); + } + catch (Exception ex) + { + Penumbra.Messager.NotificationMessage(ex, $"Could not open file {fileName}.", $"Could not open file {fileName}", + NotificationType.Warning); + } + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImGui.SetClipboardText(identifier); + + ImGuiUtil.HoverTooltip( + $"Open the file\n\t{fileName}\ncontaining this design in the .json-editor of your choice.\n\nRight-Click to copy identifier to clipboard."); + + ImGui.EndGroup(); + ImGui.Dummy(Vector2.Zero); + ImGui.Separator(); + ImGui.Dummy(Vector2.Zero); + } + + private void DrawContext(bool open, ModCollection? collection, CollectionType type, ActorIdentifier identifier, string text, char suffix) + { + var label = $"{type}{text}{suffix}"; + if (open) + ImGui.OpenPopup(label); + + using var context = ImRaii.Popup(label); + if (!context) + return; + + using (var color = ImRaii.PushColor(ImGuiCol.Text, Colors.DiscordColor)) + { + if (ImGui.MenuItem("Use no mods.")) + _active.SetCollection(ModCollection.Empty, type, _active.Individuals.GetGroup(identifier)); + } + + if (collection != null && type.CanBeRemoved()) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); + if (ImGui.MenuItem("Remove this assignment.")) + _active.SetCollection(null, type, _active.Individuals.GetGroup(identifier)); + } + + foreach (var coll in _collections.OrderBy(c => c.Identity.Name)) + { + if (coll != collection && ImGui.MenuItem($"Use {coll.Identity.Name}.")) + _active.SetCollection(coll, type, _active.Individuals.GetGroup(identifier)); + } + } + + private bool DrawButton(string text, CollectionType type, Vector2 width, uint borderColor, ActorIdentifier id, char suffix, + ModCollection? collection = null) + { + using var group = ImRaii.Group(); + var invalid = type == CollectionType.Individual && !id.IsValid; + var redundancy = _active.RedundancyCheck(type, id); + collection ??= _active.ByType(type, id); + using var color = ImRaii.PushColor(ImGuiCol.Button, + collection == null + ? ColorId.NoAssignment.Value() + : redundancy.Length > 0 + ? ColorId.RedundantAssignment.Value() + : collection == _active.Current + ? ColorId.SelectedCollection.Value() + : collection == ModCollection.Empty + ? ColorId.NoModsAssignment.Value() + : ImGui.GetColorU32(ImGuiCol.Button), !invalid) + .Push(ImGuiCol.Border, borderColor == 0 ? ImGui.GetColorU32(ImGuiCol.TextDisabled) : borderColor); + using var disabled = ImRaii.Disabled(invalid); + var button = ImGui.Button(text, width) || ImGui.IsItemClicked(ImGuiMouseButton.Right); + var hovered = redundancy.Length > 0 && ImGui.IsItemHovered(); + DrawIndividualDragSource(text, id); + DrawIndividualDragTarget(id); + if (!invalid) + { + selector.DragTargetAssignment(type, id); + var name = Name(collection); + var size = ImGui.CalcTextSize(name); + var textPos = ImGui.GetItemRectMax() - size - ImGui.GetStyle().FramePadding; + ImGui.GetWindowDrawList().AddText(textPos, ImGui.GetColorU32(ImGuiCol.Text), name); + DrawContext(button, collection, type, id, text, suffix); + } + + if (hovered) + ImGui.SetTooltip(redundancy); + + return button; + } + + private void DrawIndividualDragSource(string text, ActorIdentifier id) + { + if (!id.IsValid) + return; + + using var source = ImRaii.DragDropSource(); + if (!source) + return; + + ImGui.SetDragDropPayload("DragIndividual", null, 0); + ImGui.TextUnformatted($"Re-ordering {text}..."); + _draggedIndividualAssignment = _active.Individuals.Index(id); + } + + private void DrawIndividualDragTarget(ActorIdentifier id) + { + if (!id.IsValid) + return; + + using var target = ImRaii.DragDropTarget(); + if (!target || !ImGuiUtil.IsDropping("DragIndividual")) + return; + + var currentIdx = _active.Individuals.Index(id); + if (_draggedIndividualAssignment != -1 && currentIdx != -1) + _active.MoveIndividualCollection(_draggedIndividualAssignment, currentIdx); + _draggedIndividualAssignment = -1; + } + + private void DrawSimpleCollectionButton(CollectionType type, Vector2 width) + { + DrawButton(type.ToName(), type, width, 0, ActorIdentifier.Invalid, 's'); + Im.Line.Same(); + using (var group = ImRaii.Group()) + { + ImGuiUtil.TextWrapped(type.ToDescription()); + switch (type) + { + case CollectionType.Default: ImGui.TextUnformatted("Overruled by any other Assignment."); break; + case CollectionType.Yourself: + ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Individual ", ColorId.NewMod.Value().Color), ("Assignments.", 0)); + break; + case CollectionType.MalePlayerCharacter: + ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Male Racial Player", Colors.DiscordColor), (", ", 0), + ("Your Character", ColorId.HandledConflictMod.Value().Color), (", or ", 0), + ("Individual ", ColorId.NewMod.Value().Color), ("Assignments.", 0)); + break; + case CollectionType.FemalePlayerCharacter: + ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Female Racial Player", Colors.ReniColorActive), (", ", 0), + ("Your Character", ColorId.HandledConflictMod.Value().Color), (", or ", 0), + ("Individual ", ColorId.NewMod.Value().Color), ("Assignments.", 0)); + break; + case CollectionType.MaleNonPlayerCharacter: + ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Male Racial NPC", Colors.DiscordColor), (", ", 0), + ("Children", ColorId.FolderLine.Value().Color), (", ", 0), ("Elderly", Colors.MetaInfoText), (", or ", 0), + ("Individual ", ColorId.NewMod.Value().Color), ("Assignments.", 0)); + break; + case CollectionType.FemaleNonPlayerCharacter: + ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Female Racial NPC", Colors.ReniColorActive), (", ", 0), + ("Children", ColorId.FolderLine.Value().Color), (", ", 0), ("Elderly", Colors.MetaInfoText), (", or ", 0), + ("Individual ", ColorId.NewMod.Value().Color), ("Assignments.", 0)); + break; + } + } + + ImGui.Separator(); + } + + private void DrawAssignmentButton(CollectionType type, Vector2 width, string name, uint color) + => DrawButton(name, type, width, color, ActorIdentifier.Invalid, 's', _active.ByType(type)); + + /// Respect incognito mode for names of identifiers. + private string Name(ActorIdentifier id, string? name) + => incognito.IncognitoMode && id.Type is IdentifierType.Player or IdentifierType.Owned + ? id.Incognito(name) + : name ?? id.ToString(); + + /// Respect incognito mode for names of collections. + private string Name(ModCollection? collection) + => collection == null ? "Unassigned" : + collection == ModCollection.Empty ? "Use No Mods" : + incognito.IncognitoMode ? collection.Identity.AnonymizedName : collection.Identity.Name; + + private void DrawIndividualButton(string intro, Vector2 width, string tooltip, char suffix, params ActorIdentifier[] identifiers) + { + if (identifiers.Length > 0 && identifiers[0].IsValid) + { + DrawButton($"{intro} ({Name(identifiers[0], null)})", CollectionType.Individual, width, 0, identifiers[0], suffix); + } + else + { + if (tooltip.Length == 0 && identifiers.Length > 0) + tooltip = $"The current target {identifiers[0].PlayerName} is not valid for an assignment."; + DrawButton($"{intro} (Unavailable)", CollectionType.Individual, width, 0, ActorIdentifier.Invalid, suffix); + } + + ImGuiUtil.HoverTooltip(tooltip); + } + + private void DrawCurrentCharacter(Vector2 width) + => DrawIndividualButton("Current Character", width, string.Empty, 'c', actors.GetCurrentPlayer()); + + private void DrawCurrentTarget(Vector2 width) + => DrawIndividualButton("Current Target", width, string.Empty, 't', + actors.FromObject(targets.Target, false, true, true)); + + private void DrawNewPlayer(Vector2 width) + => DrawIndividualButton("New Player", width, _individualAssignmentUi.PlayerTooltip, 'p', + _individualAssignmentUi.PlayerIdentifiers.FirstOrDefault()); + + private void DrawNewRetainer(Vector2 width) + => DrawIndividualButton("New Bell Retainer", width, _individualAssignmentUi.RetainerTooltip, 'r', + _individualAssignmentUi.RetainerIdentifiers.FirstOrDefault()); + + private void DrawNewNpc(Vector2 width) + => DrawIndividualButton("New NPC", width, _individualAssignmentUi.NpcTooltip, 'n', + _individualAssignmentUi.NpcIdentifiers.FirstOrDefault()); + + private void DrawNewOwned(Vector2 width) + => DrawIndividualButton("New Owned NPC", width, _individualAssignmentUi.OwnedTooltip, 'o', + _individualAssignmentUi.OwnedIdentifiers.FirstOrDefault()); + + private void DrawIndividualCollections(Vector2 width) + { + for (var i = 0; i < _active.Individuals.Count; ++i) + { + var (name, ids, coll) = _active.Individuals.Assignments[i]; + DrawButton(Name(ids[0], name), CollectionType.Individual, width, 0, ids[0], 'i', coll); + + Im.Line.Same(); + if (ImGui.GetContentRegionAvail().X < width.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X + && i < _active.Individuals.Count - 1) + ImGui.NewLine(); + } + + if (_active.Individuals.Count > 0) + ImGui.NewLine(); + } + + private void DrawCollectionName(ModCollection collection) + { + ImGui.Dummy(Vector2.One); + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.MetaInfoText); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * UiHelpers.Scale); + using var f = _nameFont.Push(); + var name = Name(collection); + var size = ImGui.CalcTextSize(name).X; + var pos = ImGui.GetContentRegionAvail().X - size + ImGui.GetStyle().FramePadding.X * 2; + if (pos > 0) + ImGui.SetCursorPosX(pos / 2); + ImGuiUtil.DrawTextButton(name, Vector2.Zero, 0); + ImGui.Dummy(Vector2.One); + } + + private void DrawStatistics(ModCollection collection) + { + GatherInUse(collection); + ImGui.Separator(); + + var buttonHeight = 2 * ImGui.GetTextLineHeightWithSpacing(); + if (_inUseCache.Count == 0 && collection.Inheritance.DirectlyInheritedBy.Count == 0) + { + ImGui.Dummy(Vector2.One); + using var f = _nameFont.Push(); + ImGuiUtil.DrawTextButton("Collection is not used.", new Vector2(ImGui.GetContentRegionAvail().X, buttonHeight), + Colors.PressEnterWarningBg); + ImGui.Dummy(Vector2.One); + ImGui.Separator(); + } + else + { + var buttonWidth = new Vector2(175 * ImGuiHelpers.GlobalScale, buttonHeight); + DrawInUseStatistics(collection, buttonWidth); + DrawInheritanceStatistics(collection, buttonWidth); + } + } + + private void GatherInUse(ModCollection collection) + { + _inUseCache.Clear(); + foreach (var special in CollectionTypeExtensions.Special.Select(t => t.Item1) + .Prepend(CollectionType.Default) + .Prepend(CollectionType.Interface) + .Where(t => _active.ByType(t) == collection)) + _inUseCache.Add((special, ActorIdentifier.Invalid)); + + foreach (var (_, id, coll) in _active.Individuals.Assignments.Where(t + => t.Collection == collection && t.Identifiers.FirstOrDefault().IsValid)) + _inUseCache.Add((CollectionType.Individual, id[0])); + } + + private void DrawInUseStatistics(ModCollection collection, Vector2 buttonWidth) + { + if (_inUseCache.Count <= 0) + return; + + using (var _ = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero)) + { + ImGuiUtil.DrawTextButton("In Use By", ImGui.GetContentRegionAvail() with { Y = 0 }, 0); + } + + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale) + .Push(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero); + + foreach (var (idx, (type, id)) in _inUseCache.Index()) + { + var name = type == CollectionType.Individual ? Name(id, null) : Buttons[type].Name; + var color = Buttons.TryGetValue(type, out var p) ? p.Border : 0; + DrawButton(name, type, buttonWidth, color, id, 's', collection); + Im.Line.Same(); + if (ImGui.GetContentRegionAvail().X < buttonWidth.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X + && idx != _inUseCache.Count - 1) + ImGui.NewLine(); + } + + ImGui.NewLine(); + ImGui.Dummy(Vector2.One); + ImGui.Separator(); + } + + private void DrawInheritanceStatistics(ModCollection collection, Vector2 buttonWidth) + { + if (collection.Inheritance.DirectlyInheritedBy.Count <= 0) + return; + + using (var _ = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero)) + { + ImGuiUtil.DrawTextButton("Inherited by", ImGui.GetContentRegionAvail() with { Y = 0 }, 0); + } + + using var f = _nameFont.Push(); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.MetaInfoText); + ImGuiUtil.DrawTextButton(Name(collection.Inheritance.DirectlyInheritedBy[0]), Vector2.Zero, 0); + var constOffset = (ImGui.GetStyle().FramePadding.X + ImGuiHelpers.GlobalScale) * 2 + + ImGui.GetStyle().ItemSpacing.X + + ImGui.GetStyle().WindowPadding.X; + foreach (var parent in collection.Inheritance.DirectlyInheritedBy.Skip(1)) + { + var name = Name(parent); + var size = ImGui.CalcTextSize(name).X; + Im.Line.Same(); + if (constOffset + size >= ImGui.GetContentRegionAvail().X) + ImGui.NewLine(); + ImGuiUtil.DrawTextButton(name, Vector2.Zero, 0); + } + + ImGui.Dummy(Vector2.One); + ImGui.Separator(); + } + + private void DrawSettingsList(ModCollection collection) + { + ImGui.Dummy(Vector2.One); + var size = new Vector2(ImGui.GetContentRegionAvail().X, 10 * ImGui.GetFrameHeightWithSpacing()); + using var table = ImRaii.Table("##activeSettings", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, size); + if (!table) + return; + + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableSetupColumn("Mod Name", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Inherited From", ImGuiTableColumnFlags.WidthFixed, 5f * ImGui.GetFrameHeight()); + ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, 1.75f * ImGui.GetFrameHeight()); + ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthFixed, 2.5f * ImGui.GetFrameHeight()); + ImGui.TableHeadersRow(); + foreach (var (mod, (settings, parent)) in mods.Select(m => (m, collection.GetInheritedSettings(m.Index))) + .Where(t => t.Item2.Settings != null) + .OrderBy(t => t.m.Name)) + { + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable(mod.Name); + ImGui.TableNextColumn(); + if (parent != collection) + ImGui.TextUnformatted(Name(parent)); + ImGui.TableNextColumn(); + var enabled = settings!.Enabled; + using (var dis = ImRaii.Disabled()) + { + ImGui.Checkbox("##check", ref enabled); + } + + ImGui.TableNextColumn(); + ImGuiUtil.RightAlign(settings.Priority.ToString(), ImGui.GetStyle().WindowPadding.X); + } + } + + private void DrawInactiveSettingsList(ModCollection collection) + { + if (collection.Settings.Unused.Count == 0) + return; + + ImGui.Dummy(Vector2.One); + var text = collection.Settings.Unused.Count > 1 + ? $"Clear all {collection.Settings.Unused.Count} unused settings from deleted mods." + : "Clear the currently unused setting from a deleted mods."; + if (ImGui.Button(text, new Vector2(ImGui.GetContentRegionAvail().X, 0))) + _collections.CleanUnavailableSettings(collection); + + ImGui.Dummy(Vector2.One); + + var size = new Vector2(ImGui.GetContentRegionAvail().X, + Math.Min(10, collection.Settings.Unused.Count + 1) * ImGui.GetFrameHeightWithSpacing()); + using var table = ImRaii.Table("##inactiveSettings", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, size); + if (!table) + return; + + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); + ImGui.TableSetupColumn("Unused Mod Identifier", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, 1.75f * ImGui.GetFrameHeight()); + ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthFixed, 2.5f * ImGui.GetFrameHeight()); + ImGui.TableHeadersRow(); + string? delete = null; + foreach (var (name, settings) in collection.Settings.Unused.OrderBy(n => n.Key)) + { + using var id = ImRaii.PushId(name); + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, + "Delete this unused setting.", false, true)) + delete = name; + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable(name); + ImGui.TableNextColumn(); + var enabled = settings.Enabled; + using (var dis = ImRaii.Disabled()) + { + ImGui.Checkbox("##check", ref enabled); + } + + ImGui.TableNextColumn(); + ImGuiUtil.RightAlign(settings.Priority.ToString(), ImGui.GetStyle().WindowPadding.X); + } + + _collections.CleanUnavailableSetting(collection, delete); + ImGui.Separator(); + } + + /// Create names and border colors for special assignments. + private static IReadOnlyDictionary CreateButtons() + { + var ret = Enum.GetValues().ToDictionary(t => t, t => (t.ToName(), 0u)); + + foreach (var race in Enum.GetValues().Skip(1)) + { + var color = race switch + { + SubRace.Midlander => 0xAA5C9FE4u, + SubRace.Highlander => 0xAA5C9FE4u, + SubRace.Wildwood => 0xAA5C9F49u, + SubRace.Duskwight => 0xAA5C9F49u, + SubRace.Plainsfolk => 0xAAEF8CB6u, + SubRace.Dunesfolk => 0xAAEF8CB6u, + SubRace.SeekerOfTheSun => 0xAA8CEFECu, + SubRace.KeeperOfTheMoon => 0xAA8CEFECu, + SubRace.Seawolf => 0xAAEFE68Cu, + SubRace.Hellsguard => 0xAAEFE68Cu, + SubRace.Raen => 0xAAB5EF8Cu, + SubRace.Xaela => 0xAAB5EF8Cu, + SubRace.Helion => 0xAAFFFFFFu, + SubRace.Lost => 0xAAFFFFFFu, + SubRace.Rava => 0xAA607FA7u, + SubRace.Veena => 0xAA607FA7u, + _ => 0u, + }; + + ret[CollectionTypeExtensions.FromParts(race, Gender.Male, false)] = ($"♂ {race.ToShortName()}", color); + ret[CollectionTypeExtensions.FromParts(race, Gender.Female, false)] = ($"♀ {race.ToShortName()}", color); + ret[CollectionTypeExtensions.FromParts(race, Gender.Male, true)] = ($"♂ {race.ToShortName()} (NPC)", color); + ret[CollectionTypeExtensions.FromParts(race, Gender.Female, true)] = ($"♀ {race.ToShortName()} (NPC)", color); + } + + ret[CollectionType.MalePlayerCharacter] = ("♂ Player", 0); + ret[CollectionType.FemalePlayerCharacter] = ("♀ Player", 0); + ret[CollectionType.MaleNonPlayerCharacter] = ("♂ NPC", 0); + ret[CollectionType.FemaleNonPlayerCharacter] = ("♀ NPC", 0); + return ret; + } + + /// Create the special assignment tree in order and with free spaces. + private static IReadOnlyList<(CollectionType, bool, bool, string, uint)> CreateTree() + { + var ret = new List<(CollectionType, bool, bool, string, uint)>(Buttons.Count); + + void Add(CollectionType type, bool pre, bool post) + { + var (name, border) = Buttons[type]; + ret.Add((type, pre, post, name, border)); + } + + Add(CollectionType.Default, false, false); + Add(CollectionType.Interface, false, false); + Add(CollectionType.Inactive, false, false); + Add(CollectionType.Inactive, false, false); + Add(CollectionType.Yourself, false, true); + Add(CollectionType.Inactive, false, true); + Add(CollectionType.NonPlayerChild, false, true); + Add(CollectionType.NonPlayerElderly, false, true); + Add(CollectionType.MalePlayerCharacter, true, true); + Add(CollectionType.FemalePlayerCharacter, true, true); + Add(CollectionType.MaleNonPlayerCharacter, true, true); + Add(CollectionType.FemaleNonPlayerCharacter, true, true); + var pre = true; + foreach (var race in Enum.GetValues().Skip(1)) + { + Add(CollectionTypeExtensions.FromParts(race, Gender.Male, false), pre, !pre); + Add(CollectionTypeExtensions.FromParts(race, Gender.Female, false), pre, !pre); + Add(CollectionTypeExtensions.FromParts(race, Gender.Male, true), pre, !pre); + Add(CollectionTypeExtensions.FromParts(race, Gender.Female, true), pre, !pre); + pre = !pre; + } + + return ret; + } +} diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/FileDialogService.cs index 7ae7b56c..abfe11d2 100644 --- a/Penumbra/UI/FileDialogService.cs +++ b/Penumbra/UI/FileDialogService.cs @@ -1,157 +1,158 @@ -using Dalamud.Interface; -using Dalamud.Interface.ImGuiFileDialog; -using Dalamud.Utility; -using Dalamud.Bindings.ImGui; -using Luna; -using OtterGui; -using Penumbra.Communication; -using Penumbra.Services; - -namespace Penumbra.UI; - -public class FileDialogService : IDisposable, IUiService -{ - private readonly CommunicatorService _communicator; - private readonly FileDialogManager _manager; - private readonly ConcurrentDictionary _startPaths = new(); - private bool _isOpen; - - public FileDialogService(CommunicatorService communicator, Configuration config) - { - _communicator = communicator; - _manager = SetupFileManager(config.ModDirectory); - _communicator.ModDirectoryChanged.Subscribe(OnModDirectoryChange, ModDirectoryChanged.Priority.FileDialogService); - } - - public void OpenFilePicker(string title, string filters, Action> callback, int selectionCountMax, string? startPath, - bool forceStartPath) - { - _isOpen = true; - _manager.OpenFileDialog(title, filters, CreateCallback(title, callback), selectionCountMax, - GetStartPath(title, startPath, forceStartPath)); - } - - public void OpenFolderPicker(string title, Action callback, string? startPath, bool forceStartPath) - { - _isOpen = true; - _manager.OpenFolderDialog(title, CreateCallback(title, callback), GetStartPath(title, startPath, forceStartPath)); - } - - public void OpenSavePicker(string title, string filters, string defaultFileName, string defaultExtension, Action callback, - string? startPath, - bool forceStartPath) - { - _isOpen = true; - _manager.SaveFileDialog(title, filters, defaultFileName, defaultExtension, CreateCallback(title, callback), - GetStartPath(title, startPath, forceStartPath)); - } - - public void Close() - { - _isOpen = false; - } - - public void Reset() - { - _isOpen = false; - _manager.Reset(); - } - - public void Draw() - { - if (_isOpen) - _manager.Draw(); - } - - public void Dispose() - { - _startPaths.Clear(); - _manager.Reset(); - _communicator.ModDirectoryChanged.Unsubscribe(OnModDirectoryChange); - } - - private string? GetStartPath(string title, string? startPath, bool forceStartPath) - { - var path = !forceStartPath && _startPaths.TryGetValue(title, out var p) ? p : startPath; - if (!path.IsNullOrEmpty() && !Directory.Exists(path)) - path = null; - return path; - } - - private Action> CreateCallback(string title, Action> callback) - { - return (valid, list) => - { - _isOpen = false; - var loc = HandleRoot(GetCurrentLocation()); - _startPaths[title] = loc; - callback(valid, list.Select(HandleRoot).ToList()); - }; - } - - private Action CreateCallback(string title, Action callback) - { - return (valid, list) => - { - _isOpen = false; - var loc = HandleRoot(GetCurrentLocation()); - _startPaths[title] = loc; - callback(valid, HandleRoot(list)); - }; - } - - private static string HandleRoot(string path) - { - if (path is [_, ':']) - return path + '\\'; - - return path; - } - - // TODO: maybe change this from reflection when its public. - private string GetCurrentLocation() - => (_manager.GetType().GetField("dialog", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(_manager) as FileDialog) - ?.GetCurrentPath() - ?? "."; - - /// Set up the file selector with the right flags and custom side bar items. - private static FileDialogManager SetupFileManager(string modDirectory) - { - var fileManager = new FileDialogManager - { - AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking, - }; - - if (Functions.GetDownloadsFolder(out var downloadsFolder)) - fileManager.CustomSideBarItems.Add(("Downloads", downloadsFolder, FontAwesomeIcon.Download, -1)); - - if (Functions.GetQuickAccessFolders(out var folders)) - foreach (var (idx, (name, path)) in folders.Index()) - fileManager.CustomSideBarItems.Add(($"{name}##{idx}", path, FontAwesomeIcon.Folder, -1)); - - // Add Penumbra Root. This is not updated if the root changes right now. - fileManager.CustomSideBarItems.Add(("Root Directory", modDirectory, FontAwesomeIcon.Gamepad, 0)); - - // Remove Videos and Music. - fileManager.CustomSideBarItems.Add(("Videos", string.Empty, 0, -1)); - fileManager.CustomSideBarItems.Add(("Music", string.Empty, 0, -1)); - - return fileManager; - } - - /// Update the Root Directory link on changes. - private void OnModDirectoryChange(in ModDirectoryChanged.Arguments arguments) - { - var idx = _manager.CustomSideBarItems.IndexOf(t => t.Name == "Root Directory"); - if (idx >= 0) - _manager.CustomSideBarItems.RemoveAt(idx); - - if (!arguments.Valid) - return; - - if (idx >= 0) - _manager.CustomSideBarItems.Insert(idx, ("Root Directory", arguments.Directory, FontAwesomeIcon.Gamepad, 0)); - else - _manager.CustomSideBarItems.Add(("Root Directory", arguments.Directory, FontAwesomeIcon.Gamepad, 0)); - } -} +using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Utility; +using Dalamud.Bindings.ImGui; +using Luna; +using Penumbra.Communication; +using Penumbra.Services; + +namespace Penumbra.UI; + +public class FileDialogService : IDisposable, IUiService +{ + private readonly CommunicatorService _communicator; + private readonly FileDialogManager _manager; + private readonly ConcurrentDictionary _startPaths = new(); + private bool _isOpen; + + private readonly Func? _dialogGetter; + + public FileDialogService(CommunicatorService communicator, Configuration config) + { + _communicator = communicator; + _manager = SetupFileManager(config.ModDirectory); + _communicator.ModDirectoryChanged.Subscribe(OnModDirectoryChange, ModDirectoryChanged.Priority.FileDialogService); + + var fieldType = _manager.GetType().GetField("dialog", BindingFlags.Instance | BindingFlags.NonPublic); + _dialogGetter = fieldType is null ? null : fieldType.GetValue; + } + + public void OpenFilePicker(string title, string filters, Action> callback, int selectionCountMax, string? startPath, + bool forceStartPath) + { + _isOpen = true; + _manager.OpenFileDialog(title, filters, CreateCallback(title, callback), selectionCountMax, + GetStartPath(title, startPath, forceStartPath)); + } + + public void OpenFolderPicker(string title, Action callback, string? startPath, bool forceStartPath) + { + _isOpen = true; + _manager.OpenFolderDialog(title, CreateCallback(title, callback), GetStartPath(title, startPath, forceStartPath)); + } + + public void OpenSavePicker(string title, string filters, string defaultFileName, string defaultExtension, Action callback, + string? startPath, + bool forceStartPath) + { + _isOpen = true; + _manager.SaveFileDialog(title, filters, defaultFileName, defaultExtension, CreateCallback(title, callback), + GetStartPath(title, startPath, forceStartPath)); + } + + public void Close() + { + _isOpen = false; + } + + public void Reset() + { + _isOpen = false; + _manager.Reset(); + } + + public void Draw() + { + if (_isOpen) + _manager.Draw(); + } + + public void Dispose() + { + _startPaths.Clear(); + _manager.Reset(); + _communicator.ModDirectoryChanged.Unsubscribe(OnModDirectoryChange); + } + + private string? GetStartPath(string title, string? startPath, bool forceStartPath) + { + var path = !forceStartPath && _startPaths.TryGetValue(title, out var p) ? p : startPath; + if (!path.IsNullOrEmpty() && !Directory.Exists(path)) + path = null; + return path; + } + + private Action> CreateCallback(string title, Action> callback) + { + return (valid, list) => + { + _isOpen = false; + var loc = HandleRoot(GetCurrentLocation()); + _startPaths[title] = loc; + callback(valid, list.Select(HandleRoot).ToList()); + }; + } + + private Action CreateCallback(string title, Action callback) + { + return (valid, list) => + { + _isOpen = false; + var loc = HandleRoot(GetCurrentLocation()); + _startPaths[title] = loc; + callback(valid, HandleRoot(list)); + }; + } + + private static string HandleRoot(string path) + { + if (path is [_, ':']) + return path + '\\'; + + return path; + } + + private string GetCurrentLocation() + => (_dialogGetter?.Invoke(_manager) as FileDialog)?.GetCurrentPath() ?? "."; + + /// Set up the file selector with the right flags and custom side bar items. + private static FileDialogManager SetupFileManager(string modDirectory) + { + var fileManager = new FileDialogManager + { + AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking, + }; + + if (WindowsFunctions.GetDownloadsFolder(out var downloadsFolder)) + fileManager.CustomSideBarItems.Add(("Downloads", downloadsFolder, FontAwesomeIcon.Download, -1)); + + if (WindowsFunctions.GetQuickAccessFolders(out var folders)) + foreach (var (idx, (name, path)) in folders.Index()) + fileManager.CustomSideBarItems.Add(($"{name}##{idx}", path, FontAwesomeIcon.Folder, -1)); + + // Add Penumbra Root. This is not updated if the root changes right now. + fileManager.CustomSideBarItems.Add(("Root Directory", modDirectory, FontAwesomeIcon.Gamepad, 0)); + + // Remove Videos and Music. + fileManager.CustomSideBarItems.Add(("Videos", string.Empty, 0, -1)); + fileManager.CustomSideBarItems.Add(("Music", string.Empty, 0, -1)); + + return fileManager; + } + + /// Update the Root Directory link on changes. + private void OnModDirectoryChange(in ModDirectoryChanged.Arguments arguments) + { + var idx = _manager.CustomSideBarItems.IndexOf(t => t.Name == "Root Directory"); + if (idx >= 0) + _manager.CustomSideBarItems.RemoveAt(idx); + + if (!arguments.Valid) + return; + + if (idx >= 0) + _manager.CustomSideBarItems.Insert(idx, ("Root Directory", arguments.Directory, FontAwesomeIcon.Gamepad, 0)); + else + _manager.CustomSideBarItems.Add(("Root Directory", arguments.Directory, FontAwesomeIcon.Gamepad, 0)); + } +} diff --git a/Penumbra/UI/ImportPopup.cs b/Penumbra/UI/ImportPopup.cs index da47d0c3..626ddd40 100644 --- a/Penumbra/UI/ImportPopup.cs +++ b/Penumbra/UI/ImportPopup.cs @@ -1,33 +1,33 @@ -using Dalamud.Interface.Windowing; -using Dalamud.Bindings.ImGui; -using OtterGui.Raii; +using ImSharp; +using Luna; using Penumbra.Import.Structs; using Penumbra.Mods.Manager; namespace Penumbra.UI; /// Draw the progress information for import. -public sealed class ImportPopup : Window, Luna.IUiService +public sealed class ImportPopup : Window, IUiService { public const string WindowLabel = "Penumbra Import Status"; - private readonly ModImportManager _modImportManager; + private readonly ModImportManager _modImportManager; + private static readonly Vector2 OneHalf = Vector2.One / 2; public bool WasDrawn { get; private set; } public bool PopupWasDrawn { get; private set; } public ImportPopup(ModImportManager modImportManager) : base(WindowLabel, - ImGuiWindowFlags.NoCollapse - | ImGuiWindowFlags.NoDecoration - | ImGuiWindowFlags.NoBackground - | ImGuiWindowFlags.NoMove - | ImGuiWindowFlags.NoInputs - | ImGuiWindowFlags.NoNavFocus - | ImGuiWindowFlags.NoFocusOnAppearing - | ImGuiWindowFlags.NoBringToFrontOnFocus - | ImGuiWindowFlags.NoDocking - | ImGuiWindowFlags.NoTitleBar, true) + WindowFlags.NoCollapse + | WindowFlags.NoDecoration + | WindowFlags.NoBackground + | WindowFlags.NoMove + | WindowFlags.NoInputs + | WindowFlags.NoNavFocus + | WindowFlags.NoFocusOnAppearing + | WindowFlags.NoBringToFrontOnFocus + | WindowFlags.NoDocking + | WindowFlags.NoTitleBar, true) { _modImportManager = modImportManager; DisableWindowSounds = true; @@ -55,29 +55,28 @@ public sealed class ImportPopup : Window, Luna.IUiService if (!_modImportManager.IsImporting(out var import)) return; - const string importPopup = "##PenumbraImportPopup"; - if (!ImGui.IsPopupOpen(importPopup)) - ImGui.OpenPopup(importPopup); + if (!Im.Popup.IsOpen("##PenumbraImportPopup"u8)) + Im.Popup.Open("##PenumbraImportPopup"u8); - var display = ImGui.GetIO().DisplaySize; - var height = Math.Max(display.Y / 4, 15 * ImGui.GetFrameHeightWithSpacing()); + var display = Im.Io.DisplaySize; + var height = Math.Max(display.Y / 4, 15 * Im.Style.FrameHeightWithSpacing); var width = display.X / 8; var size = new Vector2(width * 2, height); - ImGui.SetNextWindowPos(ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, Vector2.One / 2); - ImGui.SetNextWindowSize(size); - using var popup = ImRaii.Popup(importPopup, ImGuiWindowFlags.Modal); + Im.Window.SetNextPosition(Im.Viewport.Main.Center, Condition.Always, OneHalf); + Im.Window.SetNextSize(size); + using var popup = Im.Popup.Begin("##PenumbraImportPopup"u8, WindowFlags.Modal); PopupWasDrawn = true; var terminate = false; - using (var child = ImRaii.Child("##import", new Vector2(-1, size.Y - ImGui.GetFrameHeight() * 2))) + using (var child = Im.Child.Begin("##import"u8, new Vector2(-1, size.Y - Im.Style.FrameHeight * 2))) { - if (child.Success && import.DrawProgressInfo(new Vector2(-1, ImGui.GetFrameHeight()))) - if (!ImGui.IsMouseHoveringRect(ImGui.GetWindowPos(), ImGui.GetWindowPos() + ImGui.GetWindowSize()) - && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + if (child.Success && import.DrawProgressInfo(new Vector2(-1, Im.Style.FrameHeight))) + if (!Im.Mouse.IsHoveringRectangle(Rectangle.FromSize(Im.Window.Position, Im.Window.Size)) + && Im.Mouse.IsClicked(MouseButton.Left)) terminate = true; } terminate |= import.State == ImporterState.Done - ? ImGui.Button("Close", -Vector2.UnitX) + ? Im.Button("Close"u8, -Vector2.UnitX) : import.DrawCancelButton(-Vector2.UnitX); if (terminate) _modImportManager.ClearImport(); diff --git a/Penumbra/UI/IncognitoService.cs b/Penumbra/UI/IncognitoService.cs index bd988fb3..411a5145 100644 --- a/Penumbra/UI/IncognitoService.cs +++ b/Penumbra/UI/IncognitoService.cs @@ -13,7 +13,7 @@ public class IncognitoService(TutorialService tutorial, Configuration config) : { var hold = config.IncognitoModifier.IsActive(); var color = ColorId.FolderExpanded.Value(); - using (new Im.ColorStyleDisposable().PushBorder(ImStyleBorder.Frame, color)) + using (ImStyleBorder.Frame.Push(color)) { var tt = IncognitoMode ? "Toggle incognito mode off."u8 : "Toggle incognito mode on."u8; var icon = IncognitoMode ? LunaStyle.IncognitoOn : LunaStyle.IncognitoOff; diff --git a/Penumbra/UI/Knowledge/KnowledgeWindow.cs b/Penumbra/UI/Knowledge/KnowledgeWindow.cs index 36c88833..81c91326 100644 --- a/Penumbra/UI/Knowledge/KnowledgeWindow.cs +++ b/Penumbra/UI/Knowledge/KnowledgeWindow.cs @@ -1,77 +1,74 @@ -using Dalamud.Interface.Utility.Raii; -using Dalamud.Interface.Windowing; -using Dalamud.Bindings.ImGui; -using OtterGui.Text; -using Penumbra.String; - -namespace Penumbra.UI.Knowledge; - -/// Draw the progress information for import. -public sealed class KnowledgeWindow : Window, Luna.IUiService -{ - private readonly IReadOnlyList _tabs = - [ - new RaceCodeTab(), - ]; - - private IKnowledgeTab? _selected; - private readonly byte[] _filterStore = new byte[256]; - - private ByteString _lower = ByteString.Empty; - - /// Draw the progress information for import. - public KnowledgeWindow() - : base("Penumbra Knowledge Window") - => SizeConstraints = new WindowSizeConstraints - { - MaximumSize = new Vector2(10000, 10000), - MinimumSize = new Vector2(400, 200), - }; - - public override void Draw() - { - DrawSelector(); - ImUtf8.SameLineInner(); - DrawMain(); - } - - private void DrawSelector() - { - using var group = ImUtf8.Group(); - using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0).Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero)) - { - ImGui.SetNextItemWidth(200 * ImUtf8.GlobalScale); - if (ImUtf8.InputText("##Filter"u8, _filterStore, out TerminatedByteString filter, "Filter..."u8)) - _lower = ByteString.FromSpanUnsafe(filter, true, null, null).AsciiToLowerClone(); - } - - using var child = ImUtf8.Child("KnowledgeSelector"u8, new Vector2(200 * ImUtf8.GlobalScale, ImGui.GetContentRegionAvail().Y), true); - if (!child) - return; - - foreach (var tab in _tabs) - { - if (!_lower.IsEmpty && tab.SearchTags.IndexOf(_lower.Span) < 0) - continue; - - if (ImUtf8.Selectable(tab.Name, _selected == tab)) - _selected = tab; - } - } - - private void DrawMain() - { - using var group = ImUtf8.Group(); - using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0).Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero)) - { - ImUtf8.TextFramed(_selected == null ? "No Selection"u8 : _selected.Name, ImGui.GetColorU32(ImGuiCol.FrameBg), - new Vector2(ImGui.GetContentRegionAvail().X, 0)); - } - - using var child = ImUtf8.Child("KnowledgeMain"u8, ImGui.GetContentRegionAvail(), true); - if (!child || _selected == null) - return; - - _selected.Draw(); - } -} +using ImSharp; +using Luna; +using Penumbra.String; + +namespace Penumbra.UI.Knowledge; + +/// Draw the progress information for import. +public sealed class KnowledgeWindow : Window, IUiService +{ + private readonly IReadOnlyList _tabs = + [ + new RaceCodeTab(), + ]; + + private IKnowledgeTab? _selected; + private readonly byte[] _filterStore = new byte[256]; + + private ByteString _lower = ByteString.Empty; + + /// Draw the progress information for import. + public KnowledgeWindow() + : base("Penumbra Knowledge Window") + => SizeConstraints = new WindowSizeConstraints + { + MaximumSize = new Vector2(10000, 10000), + MinimumSize = new Vector2(400, 200), + }; + + public override void Draw() + { + DrawSelector(); + Im.Line.SameInner(); + DrawMain(); + } + + private void DrawSelector() + { + using var group = Im.Group(); + using (ImStyleSingle.FrameRounding.Push(0).Push(ImStyleDouble.ItemSpacing, Vector2.Zero)) + { + Im.Item.SetNextWidthScaled(200); + if (Im.Input.Text("##Filter"u8, _filterStore, out ulong length, "Filter..."u8)) + _lower = ByteString.FromSpanUnsafe(_filterStore.AsSpan(0, (int)length), true, null, null).AsciiToLowerClone(); + } + + using var child = Im.Child.Begin("KnowledgeSelector"u8, Im.ContentRegion.Available with { X = 200 * Im.Style.GlobalScale }, true); + if (!child) + return; + + foreach (var tab in _tabs) + { + if (!_lower.IsEmpty && tab.SearchTags.IndexOf(_lower.Span) < 0) + continue; + + if (Im.Selectable(tab.Name, _selected == tab)) + _selected = tab; + } + } + + private void DrawMain() + { + using var group = Im.Group(); + using (ImStyleSingle.FrameRounding.Push(0).Push(ImStyleDouble.ItemSpacing, Vector2.Zero)) + { + ImEx.TextFramed(_selected == null ? "No Selection"u8 : _selected.Name, Im.ContentRegion.Available with { Y = 0 }); + } + + using var child = Im.Child.Begin("KnowledgeMain"u8, Im.ContentRegion.Available, true); + if (!child || _selected == null) + return; + + _selected.Draw(); + } +} diff --git a/Penumbra/UI/Knowledge/RaceCodeTab.cs b/Penumbra/UI/Knowledge/RaceCodeTab.cs index 3dbb9e27..43d838b2 100644 --- a/Penumbra/UI/Knowledge/RaceCodeTab.cs +++ b/Penumbra/UI/Knowledge/RaceCodeTab.cs @@ -1,83 +1,77 @@ -using Dalamud.Bindings.ImGui; -using ImSharp; -using OtterGui.Text; -using Penumbra.GameData.Enums; - -namespace Penumbra.UI.Knowledge; - -public sealed class RaceCodeTab() : IKnowledgeTab -{ - public ReadOnlySpan Name - => "Race Codes"u8; - - public ReadOnlySpan SearchTags - => "deformersracecodesmodel"u8; - - public void Draw() - { - var size = new Vector2((ImGui.GetContentRegionAvail().X - ImUtf8.ItemSpacing.X) / 2, 0); - using (var table = ImUtf8.Table("adults"u8, 4, ImGuiTableFlags.BordersOuter, size)) - { - if (!table) - return; - - DrawHeaders(); - foreach (var gr in Enum.GetValues()) - { - var (gender, race) = gr.Split(); - if (gender is not Gender.Male and not Gender.Female || race is ModelRace.Unknown) - continue; - - DrawRow(gender, race, false); - } - } - - Im.Line.Same(); - - using (var table = ImUtf8.Table("children"u8, 4, ImGuiTableFlags.BordersOuter, size)) - { - if (!table) - return; - - DrawHeaders(); - foreach (var race in (ReadOnlySpan) - [ModelRace.Midlander, ModelRace.Elezen, ModelRace.Miqote, ModelRace.AuRa, ModelRace.Unknown]) - { - foreach (var gender in (ReadOnlySpan) [Gender.Male, Gender.Female]) - DrawRow(gender, race, true); - } - } - - return; - - static void DrawHeaders() - { - ImGui.TableNextColumn(); - ImUtf8.TableHeader("Race"u8); - ImGui.TableNextColumn(); - ImUtf8.TableHeader("Gender"u8); - ImGui.TableNextColumn(); - ImUtf8.TableHeader("Age"u8); - ImGui.TableNextColumn(); - ImUtf8.TableHeader("Race Code"u8); - } - - static void DrawRow(Gender gender, ModelRace race, bool child) - { - var gr = child - ? Names.CombinedRace(gender is Gender.Male ? Gender.MaleNpc : Gender.FemaleNpc, race) - : Names.CombinedRace(gender, race); - ImGui.TableNextColumn(); - ImUtf8.Text(race.ToName()); - - ImGui.TableNextColumn(); - ImUtf8.Text(gender.ToName()); - - ImGui.TableNextColumn(); - ImUtf8.Text(child ? "Child"u8 : "Adult"u8); - - ImGui.TableNextColumn(); - ImUtf8.CopyOnClickSelectable(gr.ToRaceCode()); - } - } -} +using ImSharp; +using Penumbra.GameData.Enums; + +namespace Penumbra.UI.Knowledge; + +public sealed class RaceCodeTab : IKnowledgeTab +{ + public ReadOnlySpan Name + => "Race Codes"u8; + + public ReadOnlySpan SearchTags + => "deformersracecodesmodel"u8; + + public void Draw() + { + var size = new Vector2((Im.ContentRegion.Available.X - Im.Style.ItemSpacing.X) / 2, 0); + using (var table = Im.Table.Begin("adults"u8, 4, TableFlags.BordersOuter, size)) + { + if (!table) + return; + + DrawHeaders(table); + foreach (var gr in Enum.GetValues()) + { + var (gender, race) = gr.Split(); + if (gender is not Gender.Male and not Gender.Female || race is ModelRace.Unknown) + continue; + + DrawRow(table, gender, race, false); + } + } + + Im.Line.Same(); + + using (var table = Im.Table.Begin("children"u8, 4, TableFlags.BordersOuter, size)) + { + if (!table) + return; + + DrawHeaders(table); + foreach (var race in (ReadOnlySpan) + [ModelRace.Midlander, ModelRace.Elezen, ModelRace.Miqote, ModelRace.AuRa, ModelRace.Unknown]) + { + foreach (var gender in (ReadOnlySpan)[Gender.Male, Gender.Female]) + DrawRow(table, gender, race, true); + } + } + + return; + + static void DrawHeaders(in Im.TableDisposable table) + { + table.NextColumn(); + table.Header("Race"u8); + table.NextColumn(); + table.Header("Gender"u8); + table.NextColumn(); + table.Header("Age"u8); + table.NextColumn(); + table.Header("Race Code"u8); + } + + static void DrawRow(in Im.TableDisposable table, Gender gender, ModelRace race, bool child) + { + var gr = child + ? Names.CombinedRace(gender is Gender.Male ? Gender.MaleNpc : Gender.FemaleNpc, race) + : Names.CombinedRace(gender, race); + + table.DrawColumn(race.ToNameU8()); + table.DrawColumn(gender.ToNameU8()); + table.DrawColumn(child ? "Child"u8 : "Adult"u8); + + table.NextColumn(); + ImEx.CopyOnClickSelectable(gr.ToRaceCode()); + } + } +} diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 6a88922e..12dd4f90 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.DragDrop; using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; using Dalamud.Bindings.ImGui; +using ImSharp; using Luna; using OtterGui; using OtterGui.Filesystem; @@ -186,13 +187,13 @@ public sealed class ModFileSystemSelector : FileSystemSelector _config.SortMode; protected override uint ExpandedFolderColor - => ColorId.FolderExpanded.Value(); + => ColorId.FolderExpanded.Value().Color; protected override uint CollapsedFolderColor - => ColorId.FolderCollapsed.Value(); + => ColorId.FolderCollapsed.Value().Color; protected override uint FolderLineColor - => ColorId.FolderLine.Value(); + => ColorId.FolderLine.Value().Color; protected override bool FoldersDefaultOpen => _config.OpenFoldersByDefault; @@ -218,8 +219,8 @@ public sealed class ModFileSystemSelector : FileSystemSelector.Leaf leaf, in ModState state, bool selected) { var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; - using var c = ImRaii.PushColor(ImGuiCol.Text, state.Color.Tinted(state.Tint)) - .Push(ImGuiCol.HeaderHovered, 0x4000FFFF, leaf.Value.Favorite); + using var c = ImGuiColor.Text.Push(state.Color.Value().Tinted(state.Tint.Value())) + .Push(ImGuiColor.HeaderHovered, 0x4000FFFF, leaf.Value.Favorite); using var id = ImUtf8.PushId(leaf.Value.Index); ImUtf8.TreeNode(leaf.Value.Name, flags).Dispose(); if (ImGui.IsItemClicked(ImGuiMouseButton.Middle)) @@ -265,7 +266,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector ImGui.GetStyle().ItemSpacing.X) - ImGui.GetWindowDrawList().AddText(new Vector2(itemPos + offset, line), ColorId.SelectorPriority.Value(), priorityString); + ImGui.GetWindowDrawList().AddText(new Vector2(itemPos + offset, line), ColorId.SelectorPriority.Value().Color, priorityString); } } @@ -456,17 +457,17 @@ public sealed class ModFileSystemSelector : FileSystemSelector, ISer public void DrawToggleButton() { - using var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.ButtonActive), _isListOpen); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Tags.ToIconString(), new Vector2(ImGui.GetFrameHeight()), - "Add Predefined Tags...", false, true)) + using var color = ImGuiColor.Button.Push(Im.Style[ImGuiColor.ButtonActive], _isListOpen); + if (ImEx.Icon.Button(LunaStyle.TagsMarker, "Add Predefined Tags..."u8)) _isListOpen = !_isListOpen; } private void DrawToggleButtonTopRight() { - ImGui.SameLine(ImGui.GetContentRegionMax().X - - ImGui.GetFrameHeight() - - (ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ItemInnerSpacing.X : 0)); + var scrollBar = Im.Scroll.MaximumY > 0 ? Im.Style.ItemInnerSpacing.X : 0; + Im.Line.Same(Im.ContentRegion.Maximum.X - Im.Style.FrameHeight - scrollBar); DrawToggleButton(); } @@ -139,8 +132,8 @@ public sealed class PredefinedTagManager : ISavable, IReadOnlyList, ISer if (!_isListOpen) return false; - ImUtf8.Text("Predefined Tags"u8); - ImGui.Separator(); + Im.Text("Predefined Tags"u8); + Im.Separator(); var ret = false; _enabledColor = ColorId.PredefinedTagAdd.Value(); @@ -159,8 +152,8 @@ public sealed class PredefinedTagManager : ISavable, IReadOnlyList, ISer Im.Line.Same(); } - ImGui.NewLine(); - ImGui.Separator(); + Im.Line.New(); + Im.Separator(); return ret; } @@ -181,12 +174,12 @@ public sealed class PredefinedTagManager : ISavable, IReadOnlyList, ISer if (!_isListOpen) return; - ImUtf8.Text("Predefined Tags"u8); + Im.Text("Predefined Tags"u8); PrepareLists(selection); _enabledColor = ColorId.PredefinedTagAdd.Value(); _disabledColor = ColorId.PredefinedTagRemove.Value(); - using var color = new ImRaii.Color(); + using var color = new Im.ColorDisposable(); foreach (var (idx, tag) in _predefinedTags.Keys.Index()) { var alreadyContained = 0; @@ -217,22 +210,22 @@ public sealed class PredefinedTagManager : ISavable, IReadOnlyList, ISer } } - using var id = ImRaii.PushId(idx); - var buttonWidth = CalcTextButtonWidth(tag); + using var id = Im.Id.Push(idx); + var buttonWidth = new Vector2(Im.Font.CalculateButtonSize(tag).X, 0); // Prevent adding a new tag past the right edge of the popup - if (buttonWidth + ImGui.GetStyle().ItemSpacing.X >= ImGui.GetContentRegionAvail().X) - ImGui.NewLine(); + if (buttonWidth.X + Im.Style.ItemSpacing.X >= Im.ContentRegion.Available.X) + Im.Line.New(); var (usedColor, disabled, tt) = (missing, alreadyContained) switch { (> 0, _) => (_enabledColor, false, - $"Add this tag to {missing} mods.{(inModData > 0 ? $" {inModData} mods contain it in their mod tags and are untouched." : string.Empty)}"), + new StringU8($"Add this tag to {missing} mods.{(inModData > 0 ? $" {inModData} mods contain it in their mod tags and are untouched." : string.Empty)}")), (_, > 0) => (_disabledColor, false, - $"Remove this tag from {alreadyContained} mods.{(inModData > 0 ? $" {inModData} mods contain it in their mod tags and are untouched." : string.Empty)}"), - _ => (_disabledColor, true, "This tag is already present in the mod tags of all selected mods."), + new StringU8($"Remove this tag from {alreadyContained} mods.{(inModData > 0 ? $" {inModData} mods contain it in their mod tags and are untouched." : string.Empty)}")), + _ => (_disabledColor, true, new StringU8("This tag is already present in the mod tags of all selected mods.")), }; - color.Push(ImGuiCol.Button, usedColor); - if (ImUtf8.ButtonEx(tag, tt, new Vector2(buttonWidth, 0), disabled)) + color.Push(ImGuiColor.Button, usedColor); + if (ImEx.Button(tag, buttonWidth, tt, disabled)) { if (missing > 0) foreach (var (mod, (localIdx, _)) in _selectedMods.Zip(_countedMods)) @@ -256,34 +249,30 @@ public sealed class PredefinedTagManager : ISavable, IReadOnlyList, ISer color.Pop(); } - ImGui.NewLine(); + Im.Line.New(); } private bool DrawColoredButton(string buttonLabel, int index, int tagIdx, bool inOther) { - using var id = ImRaii.PushId(index); - var buttonWidth = CalcTextButtonWidth(buttonLabel); + using var id = Im.Id.Push(index); + var buttonWidth = Im.Font.CalculateButtonSize(buttonLabel).X; // Prevent adding a new tag past the right edge of the popup - if (buttonWidth + ImGui.GetStyle().ItemSpacing.X >= ImGui.GetContentRegionAvail().X) - ImGui.NewLine(); + if (buttonWidth + Im.Style.ItemSpacing.X >= Im.ContentRegion.Available.X) + Im.Line.New(); bool ret; - using (ImRaii.Disabled(inOther)) + using (Im.Disabled(inOther)) { - using var color = ImRaii.PushColor(ImGuiCol.Button, tagIdx >= 0 || inOther ? _disabledColor : _enabledColor); - ret = ImGui.Button(buttonLabel); + using var color = ImGuiColor.Button.Push(tagIdx >= 0 || inOther ? _disabledColor : _enabledColor); + ret = Im.Button(buttonLabel); } - if (inOther && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) - ImGui.SetTooltip("This tag is already present in the other set of tags."); - + if (inOther) + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "This tag is already present in the other set of tags."u8); return ret; } - private static float CalcTextButtonWidth(string text) - => ImGui.CalcTextSize(text).X + 2 * ImGui.GetStyle().FramePadding.X; - public IEnumerator GetEnumerator() => _predefinedTags.Keys.GetEnumerator(); diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index e09a1888..68d00ae2 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -2,7 +2,6 @@ using Dalamud.Bindings.ImGui; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImSharp; -using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -144,8 +143,7 @@ public sealed class ResourceWatcher : IDisposable, ITab, Luna.IUiService Im.Item.SetNextWidth(Im.ContentRegion.Available.X); var tmp = _logFilter; var invalidRegex = _logRegex is null && _logFilter.Length > 0; - using var color = - new Im.ColorStyleDisposable().PushBorder(ImStyleBorder.Frame, Colors.RegexWarningBorder, Im.Style.GlobalScale, invalidRegex); + using var color = ImStyleBorder.Frame.Push(Colors.RegexWarningBorder, Im.Style.GlobalScale, invalidRegex); if (Im.Input.Text("##logFilter"u8, ref tmp, "If path matches this Regex..."u8)) UpdateFilter(tmp, true); }