Update some files for Luna.

This commit is contained in:
Ottermandias 2025-10-19 22:24:52 +02:00
parent 3f48cd6910
commit a4302c9145
23 changed files with 1669 additions and 1712 deletions

2
Luna

@ -1 +1 @@
Subproject commit 2542a4665aca0ee7a66ad940638ff84cd04e35b5
Subproject commit 1ffa8c5de118f94b609f7d6352e3b63de463398c

@ -1 +1 @@
Subproject commit fcb443b79b0954967a959aa2f498699bd1a54923
Subproject commit 3a9406bc634228cc0815ddb6fe5e20419cafb864

View file

@ -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());
}

View file

@ -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;

View file

@ -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);
}

View file

@ -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<T>(
{
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<T>(
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<T>(
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());
}

View file

@ -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)

View file

@ -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);

View file

@ -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<char> 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<ChangedItemIconFlag, IDalamudTextureWrap> _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();
}
/// <summary> Check if a changed item should be drawn based on its category. </summary>
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));
/// <summary> Draw the icon corresponding to the category of a changed item. </summary>
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));
}
/// <summary> Draw the model information, right-justified. </summary>
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);
}
/// <summary> Draw the model information, right-justified. </summary>
public static void DrawModelData(ReadOnlySpan<byte> 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);
}
/// <summary> Draw a header line with the different icon types to filter them. </summary>
public void DrawTypeFilter()
{
if (_config.HideChangedItemFilters)
return;
var typeFilter = _config.Ephemeral.ChangedItemFilter;
if (DrawTypeFilter(ref typeFilter))
{
_config.Ephemeral.ChangedItemFilter = typeFilter;
_config.Ephemeral.Save();
}
}
/// <summary> Draw a header line with the different icon types to filter them. </summary>
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;
}
}
/// <summary> Initialize the icons. </summary>
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<TexFile>("ui/icon/062000/062044_hr1.tex")!));
Add(ChangedItemIconFlag.Demihuman, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062043_hr1.tex")!));
Add(ChangedItemIconFlag.Customization, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062045_hr1.tex")!));
Add(ChangedItemIconFlag.Action, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("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<TexFile>("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<TexFile>("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<TexFile>("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<char> 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<ChangedItemIconFlag, IDalamudTextureWrap> _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();
}
/// <summary> Check if a changed item should be drawn based on its category. </summary>
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));
/// <summary> Draw the icon corresponding to the category of a changed item. </summary>
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));
}
/// <summary> Draw the model information, right-justified. </summary>
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);
}
/// <summary> Draw the model information, right-justified. </summary>
public static void DrawModelData(ReadOnlySpan<byte> 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);
}
/// <summary> Draw a header line with the different icon types to filter them. </summary>
public void DrawTypeFilter()
{
if (_config.HideChangedItemFilters)
return;
var typeFilter = _config.Ephemeral.ChangedItemFilter;
if (DrawTypeFilter(ref typeFilter))
{
_config.Ephemeral.ChangedItemFilter = typeFilter;
_config.Ephemeral.Save();
}
}
/// <summary> Draw a header line with the different icon types to filter them. </summary>
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;
}
}
/// <summary> Initialize the icons. </summary>
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<TexFile>("ui/icon/062000/062044_hr1.tex")!));
Add(ChangedItemIconFlag.Demihuman, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062043_hr1.tex")!));
Add(ChangedItemIconFlag.Customization, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062045_hr1.tex")!));
Add(ChangedItemIconFlag.Action, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("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<TexFile>("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<TexFile>("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<TexFile>("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");
}
}

View file

@ -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<byte> 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)

View file

@ -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;
/// <summary> Draw the header line that can quick switch between collections. </summary>
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<Penumbra.Collections.ModCollection?, ImSharp.Utf8StringHandler<ImSharp.LabelStringHandlerBuffer>,
ImSharp.Utf8StringHandler<ImSharp.TextStringHandlerBuffer>, 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;
/// <summary> Draw the header line that can quick switch between collections. </summary>
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();
}
}

View file

@ -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<ColorId, uint> _colors = new Dictionary<ColorId, uint>();
private static Dictionary<ColorId, uint> _colors = [];
/// <summary> Obtain the configured value for a color. </summary>
public static uint Value(this ColorId color)
public static Rgba32 Value(this ColorId color)
=> _colors.TryGetValue(color, out var value) ? value : color.Data().DefaultColor;
/// <summary> Set the configurable colors dictionary to a value. </summary>

File diff suppressed because it is too large Load diff

View file

@ -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<string, string> _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<bool, List<string>> 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<bool, string> 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<bool, string> 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<bool, List<string>> CreateCallback(string title, Action<bool, List<string>> callback)
{
return (valid, list) =>
{
_isOpen = false;
var loc = HandleRoot(GetCurrentLocation());
_startPaths[title] = loc;
callback(valid, list.Select(HandleRoot).ToList());
};
}
private Action<bool, string> CreateCallback(string title, Action<bool, string> 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()
?? ".";
/// <summary> Set up the file selector with the right flags and custom side bar items. </summary>
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;
}
/// <summary> Update the Root Directory link on changes. </summary>
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<string, string> _startPaths = new();
private bool _isOpen;
private readonly Func<object?, object?>? _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<bool, List<string>> 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<bool, string> 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<bool, string> 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<bool, List<string>> CreateCallback(string title, Action<bool, List<string>> callback)
{
return (valid, list) =>
{
_isOpen = false;
var loc = HandleRoot(GetCurrentLocation());
_startPaths[title] = loc;
callback(valid, list.Select(HandleRoot).ToList());
};
}
private Action<bool, string> CreateCallback(string title, Action<bool, string> 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() ?? ".";
/// <summary> Set up the file selector with the right flags and custom side bar items. </summary>
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;
}
/// <summary> Update the Root Directory link on changes. </summary>
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));
}
}

View file

@ -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;
/// <summary> Draw the progress information for import. </summary>
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();

View file

@ -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;

View file

@ -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;
/// <summary> Draw the progress information for import. </summary>
public sealed class KnowledgeWindow : Window, Luna.IUiService
{
private readonly IReadOnlyList<IKnowledgeTab> _tabs =
[
new RaceCodeTab(),
];
private IKnowledgeTab? _selected;
private readonly byte[] _filterStore = new byte[256];
private ByteString _lower = ByteString.Empty;
/// <summary> Draw the progress information for import. </summary>
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;
/// <summary> Draw the progress information for import. </summary>
public sealed class KnowledgeWindow : Window, IUiService
{
private readonly IReadOnlyList<IKnowledgeTab> _tabs =
[
new RaceCodeTab(),
];
private IKnowledgeTab? _selected;
private readonly byte[] _filterStore = new byte[256];
private ByteString _lower = ByteString.Empty;
/// <summary> Draw the progress information for import. </summary>
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();
}
}

View file

@ -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<byte> Name
=> "Race Codes"u8;
public ReadOnlySpan<byte> 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<GenderRace>())
{
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>)
[ModelRace.Midlander, ModelRace.Elezen, ModelRace.Miqote, ModelRace.AuRa, ModelRace.Unknown])
{
foreach (var gender in (ReadOnlySpan<Gender>) [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<byte> Name
=> "Race Codes"u8;
public ReadOnlySpan<byte> 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<GenderRace>())
{
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>)
[ModelRace.Midlander, ModelRace.Elezen, ModelRace.Miqote, ModelRace.AuRa, ModelRace.Unknown])
{
foreach (var gender in (ReadOnlySpan<Gender>)[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());
}
}
}

View file

@ -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<Mod, ModFileSyste
=> _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<Mod, ModFileSyste
protected override void DrawLeafName(FileSystem<Mod>.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<Mod, ModFileSyste
offset -= ImGui.GetStyle().ItemInnerSpacing.X;
if (offset > 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<Mod, ModFileSyste
ImUtf8.BulletText("Select a mod to obtain more information or change settings."u8);
ImUtf8.BulletText("Names are colored according to your config and their current state in the collection:"u8);
indent.Push();
ImUtf8.BulletTextColored(ColorId.EnabledMod.Value(), "enabled in the current collection."u8);
ImUtf8.BulletTextColored(ColorId.DisabledMod.Value(), "disabled in the current collection."u8);
ImUtf8.BulletTextColored(ColorId.InheritedMod.Value(), "enabled due to inheritance from another collection."u8);
ImUtf8.BulletTextColored(ColorId.InheritedDisabledMod.Value(), "disabled due to inheritance from another collection."u8);
ImUtf8.BulletTextColored(ColorId.UndefinedMod.Value(), "unconfigured in all inherited collections."u8);
ImUtf8.BulletTextColored(ColorId.HandledConflictMod.Value(),
ImUtf8.BulletTextColored(ColorId.EnabledMod.Value().Color, "enabled in the current collection."u8);
ImUtf8.BulletTextColored(ColorId.DisabledMod.Value().Color, "disabled in the current collection."u8);
ImUtf8.BulletTextColored(ColorId.InheritedMod.Value().Color, "enabled due to inheritance from another collection."u8);
ImUtf8.BulletTextColored(ColorId.InheritedDisabledMod.Value().Color, "disabled due to inheritance from another collection."u8);
ImUtf8.BulletTextColored(ColorId.UndefinedMod.Value().Color, "unconfigured in all inherited collections."u8);
ImUtf8.BulletTextColored(ColorId.HandledConflictMod.Value().Color,
"enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)."u8);
ImUtf8.BulletTextColored(ColorId.ConflictingMod.Value(),
ImUtf8.BulletTextColored(ColorId.ConflictingMod.Value().Color,
"enabled and conflicting with another enabled Mod on the same priority."u8);
ImUtf8.BulletTextColored(ColorId.FolderExpanded.Value(), "expanded mod folder."u8);
ImUtf8.BulletTextColored(ColorId.FolderCollapsed.Value(), "collapsed mod folder"u8);
ImUtf8.BulletTextColored(ColorId.FolderExpanded.Value().Color, "expanded mod folder."u8);
ImUtf8.BulletTextColored(ColorId.FolderCollapsed.Value().Color, "collapsed mod folder"u8);
indent.Pop(1);
ImUtf8.BulletText("Middle-click a mod to disable it if it is enabled or enable it if it is disabled."u8);
indent.Push();

View file

@ -124,7 +124,7 @@ public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSele
: settings.Enabled
? (parent == collection ? enabled : inherited, ModState.Enabled)
: (parent == collection ? disabled : disInherited, ModState.Disabled);
_cache.Add((collection, parent, color, text));
_cache.Add((collection, parent, color.Color, text));
if (color == enabled)
++directCount;

View file

@ -75,7 +75,7 @@ public class ModPanelSettingsTab(
if (!_temporary)
return;
using var color = ImRaii.PushColor(ImGuiCol.Button, ImGuiCol.Button.Tinted(ColorId.TemporaryModSettingsTint));
using var color = ImGuiColor.Button.Push(Rgba32.TintColor(Im.Style[ImGuiColor.Button], ColorId.TemporaryModSettingsTint.Value().ToVector()));
var width = new Vector2(ImGui.GetContentRegionAvail().X, 0);
if (ImUtf8.ButtonEx($"These settings are temporarily set by {selection.TemporarySettings!.Source}{(_locked ? " and locked." : ".")}",
width,

View file

@ -1,13 +1,8 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification;
using ImSharp;
using Luna;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Text;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
@ -116,17 +111,15 @@ public sealed class PredefinedTagManager : ISavable, IReadOnlyList<string>, 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<string>, 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<string>, 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<string>, 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<string>, 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<string>, 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<string> GetEnumerator()
=> _predefinedTags.Keys.GetEnumerator();

View file

@ -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);
}