Penumbra/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs
Ottermandias 5a2fddab89
Some checks are pending
.NET Build / build (push) Waiting to run
More stuff to Luna.
2025-11-02 14:55:57 +01:00

370 lines
14 KiB
C#

using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Bindings.ImGui;
using ImSharp;
using Luna;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Text.EndObjects;
using Penumbra.Meta;
using Penumbra.Mods;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using Penumbra.Services;
using Penumbra.UI.Classes;
namespace Penumbra.UI.ModsTab.Groups;
public sealed class ModGroupEditDrawer(
ModManager modManager,
Configuration config,
FilenameService filenames,
DescriptionEditPopup descriptionPopup,
ImcChecker imcChecker) : IUiService
{
private static ReadOnlySpan<byte> AcrossGroupsLabel
=> "##DragOptionAcross"u8;
private static ReadOnlySpan<byte> InsideGroupLabel
=> "##DragOptionInside"u8;
internal readonly ImcChecker ImcChecker = imcChecker;
internal readonly ModManager ModManager = modManager;
internal readonly Queue<Action> ActionQueue = new();
internal Vector2 OptionIdxSelectable;
internal Vector2 AvailableWidth;
internal float PriorityWidth;
internal string? NewOptionName;
private IModGroup? _newOptionGroup;
private Vector2 _buttonSize;
private float _groupNameWidth;
private float _optionNameWidth;
private float _spacing;
private bool _deleteEnabled;
private string? _currentGroupName;
private ModPriority? _currentGroupPriority;
private IModGroup? _currentGroupEdited;
private bool _isGroupNameValid = true;
private IModGroup? _dragDropGroup;
private IModOption? _dragDropOption;
private bool _draggingAcross;
internal string? CombiningDisplayName;
internal int CombiningDisplayIndex;
public void Draw(Mod mod)
{
PrepareStyle();
using var id = ImUtf8.PushId("##GroupEdit"u8);
foreach (var (groupIdx, group) in mod.Groups.Index())
DrawGroup(group, groupIdx);
while (ActionQueue.TryDequeue(out var action))
action.Invoke();
}
private void DrawGroup(IModGroup group, int idx)
{
using var id = ImUtf8.PushId(idx);
using var frame = ImRaii.FramedGroup($"Group #{idx + 1}");
DrawGroupNameRow(group, idx);
group.EditDrawer(this).Draw();
}
private void DrawGroupNameRow(IModGroup group, int idx)
{
DrawGroupName(group);
Im.Line.SameInner();
DrawGroupMoveButtons(group, idx);
Im.Line.SameInner();
DrawGroupOpenFile(group, idx);
Im.Line.SameInner();
DrawGroupDescription(group);
Im.Line.SameInner();
DrawGroupDelete(group);
Im.Line.SameInner();
DrawGroupPriority(group);
}
private void DrawGroupName(IModGroup group)
{
var text = _currentGroupEdited == group ? _currentGroupName ?? group.Name : group.Name;
Im.Item.SetNextWidth(_groupNameWidth);
using var border = ImRaii.PushFrameBorder(Im.Style.GlobalScale * 2, Colors.RegexWarningBorder, !_isGroupNameValid);
if (ImUtf8.InputText("##GroupName"u8, ref text))
{
_currentGroupEdited = group;
_currentGroupName = text;
_isGroupNameValid = text == group.Name || ModGroupEditor.VerifyFileName(group.Mod, group, text, false);
}
if (ImGui.IsItemDeactivated())
{
if (_currentGroupName != null && _isGroupNameValid)
ModManager.OptionEditor.RenameModGroup(group, _currentGroupName);
_currentGroupName = null;
_currentGroupEdited = null;
_isGroupNameValid = true;
}
var tt = _isGroupNameValid
? "Change the Group name."u8
: "Current name can not be used for this group."u8;
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, tt);
}
private void DrawGroupDelete(IModGroup group)
{
if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled))
ActionQueue.Enqueue(() => ModManager.OptionEditor.DeleteModGroup(group));
if (_deleteEnabled)
Im.Tooltip.OnHover("Delete this option group."u8);
else
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled,
$"Delete this option group.\nHold {config.DeleteModModifier} while clicking to delete.");
}
private void DrawGroupPriority(IModGroup group)
{
var priority = _currentGroupEdited == group
? (_currentGroupPriority ?? group.Priority).Value
: group.Priority.Value;
Im.Item.SetNextWidth(PriorityWidth);
if (ImGui.InputInt("##GroupPriority", ref priority))
{
_currentGroupEdited = group;
_currentGroupPriority = new ModPriority(priority);
}
if (ImGui.IsItemDeactivated())
{
if (_currentGroupPriority.HasValue)
ModManager.OptionEditor.ChangeGroupPriority(group, _currentGroupPriority.Value);
_currentGroupEdited = null;
_currentGroupPriority = null;
}
ImGuiUtil.HoverTooltip("Group Priority");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DrawGroupDescription(IModGroup group)
{
if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit group description."u8))
descriptionPopup.Open(group);
}
private void DrawGroupMoveButtons(IModGroup group, int idx)
{
var isFirst = idx == 0;
if (ImUtf8.IconButton(FontAwesomeIcon.ArrowUp, isFirst))
ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveModGroup(group, idx - 1));
if (isFirst)
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Can not move this group further upwards."u8);
else
Im.Tooltip.OnHover($"Move this group up to group {idx}.");
Im.Line.SameInner();
var isLast = idx == group.Mod.Groups.Count - 1;
if (ImUtf8.IconButton(FontAwesomeIcon.ArrowDown, isLast))
ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveModGroup(group, idx + 1));
if (isLast)
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Can not move this group further downwards."u8);
else
Im.Tooltip.OnHover($"Move this group down to group {idx + 2}.");
}
private void DrawGroupOpenFile(IModGroup group, int idx)
{
var fileName = filenames.OptionGroupFile(group.Mod, idx, config.ReplaceNonAsciiOnImport);
var fileExists = File.Exists(fileName);
if (ImUtf8.IconButton(FontAwesomeIcon.FileExport, !fileExists))
try
{
Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true });
}
catch (Exception e)
{
Penumbra.Messager.NotificationMessage(e, "Could not open editor.", NotificationType.Error);
}
if (fileExists)
Im.Tooltip.OnHover($"Open the {group.Name} json file in the text editor of your choice.");
else
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"The {group.Name} json file does not exist.");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void DrawOptionPosition(IModGroup group, IModOption option, int optionIdx)
{
ImGui.AlignTextToFramePadding();
ImUtf8.Selectable($"Option #{optionIdx + 1}", false, size: OptionIdxSelectable);
Target(group, optionIdx);
Source(option);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void DrawOptionDefaultSingleBehaviour(IModGroup group, IModOption option, int optionIdx)
{
var isDefaultOption = group.DefaultSettings.AsIndex == optionIdx;
if (ImUtf8.RadioButton("##default"u8, isDefaultOption))
ModManager.OptionEditor.ChangeModGroupDefaultOption(group, Setting.Single(optionIdx));
Im.Tooltip.OnHover($"Set {option.Name} as the default choice for this group.");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void DrawOptionDefaultMultiBehaviour(IModGroup group, IModOption option, int optionIdx)
{
var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx);
if (ImUtf8.Checkbox("##default"u8, ref isDefaultOption))
ModManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption));
Im.Tooltip.OnHover($"{(isDefaultOption ? "Disable"u8 : "Enable"u8)} {option.Name} per default in this group.");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void DrawOptionDescription(IModOption option)
{
if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit option description."u8))
descriptionPopup.Open(option);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void DrawOptionPriority(MultiSubMod option)
{
var priority = option.Priority.Value;
Im.Item.SetNextWidth(PriorityWidth);
if (ImUtf8.InputScalarOnDeactivated("##Priority"u8, ref priority))
ModManager.OptionEditor.MultiEditor.ChangeOptionPriority(option, new ModPriority(priority));
Im.Tooltip.OnHover("Option priority inside the mod."u8);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void DrawOptionName(IModOption option)
{
var name = option.Name;
Im.Item.SetNextWidth(_optionNameWidth);
if (ImUtf8.InputTextOnDeactivated("##Name"u8, ref name))
ModManager.OptionEditor.RenameOption(option, name);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void DrawOptionDelete(IModOption option)
{
if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled))
ActionQueue.Enqueue(() => ModManager.OptionEditor.DeleteOption(option));
if (_deleteEnabled)
Im.Tooltip.OnHover("Delete this option."u8);
else
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled,
$"Delete this option.\nHold {config.DeleteModModifier} while clicking to delete.");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal string DrawNewOptionBase(IModGroup group, int count)
{
ImGui.AlignTextToFramePadding();
ImUtf8.Selectable($"Option #{count + 1}", false, size: OptionIdxSelectable);
Target(group, count);
Im.Line.SameInner();
ImUtf8.IconDummy();
Im.Line.SameInner();
Im.Item.SetNextWidth(_optionNameWidth);
var newName = _newOptionGroup == group
? NewOptionName ?? string.Empty
: string.Empty;
if (ImUtf8.InputText("##newOption"u8, ref newName, "Add new option..."u8))
{
NewOptionName = newName;
_newOptionGroup = group;
}
Im.Line.SameInner();
return newName;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Source(IModOption option)
{
using var source = ImUtf8.DragDropSource();
if (!source)
return;
var across = option.Group is ITexToolsGroup;
if (!DragDropSource.SetPayload(across ? AcrossGroupsLabel : InsideGroupLabel))
{
_dragDropGroup = option.Group;
_dragDropOption = option;
_draggingAcross = across;
}
ImUtf8.Text($"Dragging option {option.Name} from group {option.Group.Name}...");
}
private void Target(IModGroup group, int optionIdx)
{
if (_dragDropGroup != group
&& (!_draggingAcross || (_dragDropGroup != null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions })))
return;
using var target = ImUtf8.DragDropTarget();
if (!target.IsDropping(_draggingAcross ? AcrossGroupsLabel : InsideGroupLabel))
return;
if (_dragDropGroup != null && _dragDropOption != null)
{
if (_dragDropGroup == group)
{
var sourceOption = _dragDropOption;
ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveOption(sourceOption, optionIdx));
}
else
{
// Move from one group to another by deleting, then adding, then moving the option.
var sourceOption = _dragDropOption;
ActionQueue.Enqueue(() =>
{
ModManager.OptionEditor.DeleteOption(sourceOption);
if (ModManager.OptionEditor.AddOption(group, sourceOption) is { } newOption)
ModManager.OptionEditor.MoveOption(newOption, optionIdx);
});
}
}
_dragDropGroup = null;
_dragDropOption = null;
_draggingAcross = false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void PrepareStyle()
{
var totalWidth = 400f * Im.Style.GlobalScale;
_buttonSize = new Vector2(ImUtf8.FrameHeight);
PriorityWidth = 50 * Im.Style.GlobalScale;
AvailableWidth = new Vector2(totalWidth + 3 * _spacing + 2 * _buttonSize.X + PriorityWidth, 0);
_groupNameWidth = totalWidth - 3 * (_buttonSize.X + _spacing);
_spacing = Im.Style.ItemInnerSpacing.X;
OptionIdxSelectable = ImUtf8.CalcTextSize("Option #88."u8);
_optionNameWidth = totalWidth - OptionIdxSelectable.X - _buttonSize.X - 2 * _spacing;
_deleteEnabled = config.DeleteModModifier.IsActive();
}
}