Penumbra/Penumbra/UI/CollectionTab/InheritanceUi.cs

303 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.UI.Classes;
namespace Penumbra.UI.CollectionTab;
public class InheritanceUi
{
private const int InheritedCollectionHeight = 9;
private const string InheritanceDragDropLabel = "##InheritanceMove";
private readonly CollectionManager _collectionManager;
public InheritanceUi(CollectionManager collectionManager)
=> _collectionManager = collectionManager;
/// <summary> Draw the whole inheritance block. </summary>
public void Draw()
{
using var group = ImRaii.Group();
using var id = ImRaii.PushId("##Inheritance");
ImGui.TextUnformatted($"The {TutorialService.SelectedCollection} inherits from:");
DrawCurrentCollectionInheritance();
DrawInheritanceTrashButton();
DrawNewInheritanceSelection();
DelayedActions();
}
// Keep for reuse.
private readonly HashSet<ModCollection> _seenInheritedCollections = new(32);
// Execute changes only outside of loops.
private ModCollection? _newInheritance;
private ModCollection? _movedInheritance;
private (int, int)? _inheritanceAction;
private ModCollection? _newCurrentCollection;
/// <summary>
/// If an inherited collection is expanded,
/// draw all its flattened, distinct children in order with a tree-line.
/// </summary>
private void DrawInheritedChildren(ModCollection collection)
{
using var id = ImRaii.PushId(collection.Index);
using var indent = ImRaii.PushIndent();
// Get start point for the lines (top of the selector).
// Tree line stuff.
var lineStart = ImGui.GetCursorScreenPos();
var offsetX = -ImGui.GetStyle().IndentSpacing + ImGui.GetTreeNodeToLabelSpacing() / 2;
var drawList = ImGui.GetWindowDrawList();
var lineSize = Math.Max(0, ImGui.GetStyle().IndentSpacing - 9 * UiHelpers.Scale);
lineStart.X += offsetX;
lineStart.Y -= 2 * UiHelpers.Scale;
var lineEnd = lineStart;
// Skip the collection itself.
foreach (var inheritance in collection.GetFlattenedInheritance().Skip(1))
{
// Draw the child, already seen collections are colored as conflicts.
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(Penumbra.Config),
_seenInheritedCollections.Contains(inheritance));
_seenInheritedCollections.Add(inheritance);
ImRaii.TreeNode(inheritance.Name, ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet);
var (minRect, maxRect) = (ImGui.GetItemRectMin(), ImGui.GetItemRectMax());
DrawInheritanceTreeClicks(inheritance, false);
// Tree line stuff.
if (minRect.X == 0)
continue;
// Draw the notch and increase the line length.
var midPoint = (minRect.Y + maxRect.Y) / 2f - 1f;
drawList.AddLine(new Vector2(lineStart.X, midPoint), new Vector2(lineStart.X + lineSize, midPoint), Colors.MetaInfoText,
UiHelpers.Scale);
lineEnd.Y = midPoint;
}
// Finally, draw the folder line.
drawList.AddLine(lineStart, lineEnd, Colors.MetaInfoText, UiHelpers.Scale);
}
/// <summary> Draw a single primary inherited collection. </summary>
private void DrawInheritance(ModCollection collection)
{
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(Penumbra.Config),
_seenInheritedCollections.Contains(collection));
_seenInheritedCollections.Add(collection);
using var tree = ImRaii.TreeNode(collection.Name, ImGuiTreeNodeFlags.NoTreePushOnOpen);
color.Pop();
DrawInheritanceTreeClicks(collection, true);
DrawInheritanceDropSource(collection);
DrawInheritanceDropTarget(collection);
if (tree)
DrawInheritedChildren(collection);
else
// We still want to keep track of conflicts.
_seenInheritedCollections.UnionWith(collection.GetFlattenedInheritance());
}
/// <summary> Draw the list box containing the current inheritance information. </summary>
private void DrawCurrentCollectionInheritance()
{
using var list = ImRaii.ListBox("##inheritanceList",
new Vector2(UiHelpers.InputTextMinusButton, ImGui.GetTextLineHeightWithSpacing() * InheritedCollectionHeight));
if (!list)
return;
_seenInheritedCollections.Clear();
_seenInheritedCollections.Add(_collectionManager.Active.Current);
foreach (var collection in _collectionManager.Active.Current.DirectlyInheritsFrom.ToList())
DrawInheritance(collection);
}
/// <summary> Draw a drag and drop button to delete. </summary>
private void DrawInheritanceTrashButton()
{
ImGui.SameLine();
var size = UiHelpers.IconButtonSize with { Y = ImGui.GetTextLineHeightWithSpacing() * InheritedCollectionHeight };
var buttonColor = ImGui.GetColorU32(ImGuiCol.Button);
// Prevent hovering from highlighting the button.
using var color = ImRaii.PushColor(ImGuiCol.ButtonActive, buttonColor)
.Push(ImGuiCol.ButtonHovered, buttonColor);
ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), size,
"Drag primary inheritance here to remove it from the list.", false, true);
using var target = ImRaii.DragDropTarget();
if (target.Success && ImGuiUtil.IsDropping(InheritanceDragDropLabel))
_inheritanceAction = (_collectionManager.Active.Current.DirectlyInheritsFrom.IndexOf(_movedInheritance!), -1);
}
/// <summary>
/// Set the current collection, or delete or move an inheritance if the action was triggered during iteration.
/// Can not be done during iteration to keep collections unchanged.
/// </summary>
private void DelayedActions()
{
if (_newCurrentCollection != null)
{
_collectionManager.Active.SetCollection(_newCurrentCollection, CollectionType.Current);
_newCurrentCollection = null;
}
if (_inheritanceAction == null)
return;
if (_inheritanceAction.Value.Item1 >= 0)
{
if (_inheritanceAction.Value.Item2 == -1)
_collectionManager.Inheritances.RemoveInheritance(_collectionManager.Active.Current, _inheritanceAction.Value.Item1);
else
_collectionManager.Inheritances.MoveInheritance(_collectionManager.Active.Current, _inheritanceAction.Value.Item1, _inheritanceAction.Value.Item2);
}
_inheritanceAction = null;
}
/// <summary>
/// Draw the selector to add new inheritances.
/// The add button is only available if the selected collection can actually be added.
/// </summary>
private void DrawNewInheritanceSelection()
{
DrawNewInheritanceCombo();
ImGui.SameLine();
var inheritance = InheritanceManager.CheckValidInheritance(_collectionManager.Active.Current, _newInheritance);
var tt = inheritance switch
{
InheritanceManager.ValidInheritance.Empty => "No valid collection to inherit from selected.",
InheritanceManager.ValidInheritance.Valid => $"Let the {TutorialService.SelectedCollection} inherit from this collection.",
InheritanceManager.ValidInheritance.Self => "The collection can not inherit from itself.",
InheritanceManager.ValidInheritance.Contained => "Already inheriting from this collection.",
InheritanceManager.ValidInheritance.Circle => "Inheriting from this collection would lead to cyclic inheritance.",
_ => string.Empty,
};
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, tt,
inheritance != InheritanceManager.ValidInheritance.Valid, true)
&& _collectionManager.Inheritances.AddInheritance(_collectionManager.Active.Current, _newInheritance!))
_newInheritance = null;
if (inheritance != InheritanceManager.ValidInheritance.Valid)
_newInheritance = null;
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.QuestionCircle.ToIconString(), UiHelpers.IconButtonSize, "What is Inheritance?",
false, true))
ImGui.OpenPopup("InheritanceHelp");
ImGuiUtil.HelpPopup("InheritanceHelp", new Vector2(1000 * UiHelpers.Scale, 21 * ImGui.GetTextLineHeightWithSpacing()), () =>
{
ImGui.NewLine();
ImGui.TextWrapped(
"Inheritance is a way to use a baseline of mods across multiple collections, without needing to change all those collections if you want to add a single mod.");
ImGui.NewLine();
ImGui.TextUnformatted("Every mod in a collection can have three basic states: 'Enabled', 'Disabled' and 'Unconfigured'.");
ImGui.BulletText("If the mod is 'Enabled' or 'Disabled', it does not matter if the collection inherits from other collections.");
ImGui.BulletText(
"If the mod is unconfigured, those inherited-from collections are checked in the order displayed here, including sub-inheritances.");
ImGui.BulletText(
"If a collection is found in which the mod is either 'Enabled' or 'Disabled', the settings from this collection will be used.");
ImGui.BulletText("If no such collection is found, the mod will be treated as disabled.");
ImGui.BulletText(
"Highlighted collections in the left box are never reached because they are already checked in a sub-inheritance before.");
ImGui.NewLine();
ImGui.TextUnformatted("Example");
ImGui.BulletText("Collection A has the Bibo+ body and a Hempen Camise mod enabled.");
ImGui.BulletText(
"Collection B inherits from A, leaves Bibo+ unconfigured, but has the Hempen Camise enabled with different settings than A.");
ImGui.BulletText("Collection C also inherits from A, has Bibo+ explicitly disabled and the Hempen Camise unconfigured.");
ImGui.BulletText("Collection D inherits from C and then B and leaves everything unconfigured.");
using var indent = ImRaii.PushIndent();
ImGui.BulletText("B uses Bibo+ settings from A and its own Hempen Camise settings.");
ImGui.BulletText("C has Bibo+ disabled and uses A's Hempen Camise settings.");
ImGui.BulletText(
"D has Bibo+ disabled and uses A's Hempen Camise settings, not B's. It traversed the collections in Order D -> (C -> A) -> (B -> A).");
});
}
/// <summary>
/// Draw the combo to select new potential inheritances.
/// Only valid inheritances are drawn in the preview, or nothing if no inheritance is available.
/// </summary>
private void DrawNewInheritanceCombo()
{
ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton);
_newInheritance ??= _collectionManager.Storage.FirstOrDefault(c
=> c != _collectionManager.Active.Current && !_collectionManager.Active.Current.DirectlyInheritsFrom.Contains(c))
?? ModCollection.Empty;
using var combo = ImRaii.Combo("##newInheritance", _newInheritance.Name);
if (!combo)
return;
foreach (var collection in _collectionManager.Storage
.Where(c => InheritanceManager.CheckValidInheritance(_collectionManager.Active.Current, c) == InheritanceManager.ValidInheritance.Valid)
.OrderBy(c => c.Name))
{
if (ImGui.Selectable(collection.Name, _newInheritance == collection))
_newInheritance = collection;
}
}
/// <summary>
/// Move an inherited collection when dropped onto another.
/// Move is delayed due to collection changes.
/// </summary>
private void DrawInheritanceDropTarget(ModCollection collection)
{
using var target = ImRaii.DragDropTarget();
if (!target.Success || !ImGuiUtil.IsDropping(InheritanceDragDropLabel))
return;
if (_movedInheritance != null)
{
var idx1 = _collectionManager.Active.Current.DirectlyInheritsFrom.IndexOf(_movedInheritance);
var idx2 = _collectionManager.Active.Current.DirectlyInheritsFrom.IndexOf(collection);
if (idx1 >= 0 && idx2 >= 0)
_inheritanceAction = (idx1, idx2);
}
_movedInheritance = null;
}
/// <summary> Move an inherited collection. </summary>
private void DrawInheritanceDropSource(ModCollection collection)
{
using var source = ImRaii.DragDropSource();
if (!source)
return;
ImGui.SetDragDropPayload(InheritanceDragDropLabel, nint.Zero, 0);
_movedInheritance = collection;
ImGui.TextUnformatted($"Moving {_movedInheritance?.Name ?? "Unknown"}...");
}
/// <summary>
/// Ctrl + Right-Click -> Switch current collection to this (for all).
/// Ctrl + Shift + Right-Click -> Delete this inheritance (only if withDelete).
/// Deletion is delayed due to collection changes.
/// </summary>
private void DrawInheritanceTreeClicks(ModCollection collection, bool withDelete)
{
if (ImGui.GetIO().KeyCtrl && ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
if (withDelete && ImGui.GetIO().KeyShift)
_inheritanceAction = (_collectionManager.Active.Current.DirectlyInheritsFrom.IndexOf(collection), -1);
else
_newCurrentCollection = collection;
}
ImGuiUtil.HoverTooltip($"Control + Right-Click to switch the {TutorialService.SelectedCollection} to this one."
+ (withDelete ? "\nControl + Shift + Right-Click to remove this inheritance." : string.Empty));
}
}