11b4c268f0
Co-authored-by: Cursor <cursoragent@cursor.com>
321 lines
12 KiB
C#
321 lines
12 KiB
C#
using Dalamud.Logging;
|
|
using HSUI.Config.Attributes;
|
|
using HSUI.Config.Profiles;
|
|
using HSUI.Helpers;
|
|
using HSUI.Interface;
|
|
using Dalamud.Bindings.ImGui;
|
|
using Newtonsoft.Json;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using System.Reflection;
|
|
|
|
namespace HSUI.Config.Tree
|
|
{
|
|
public class ConfigPageNode : SubSectionNode
|
|
{
|
|
private PluginConfigObject _configObject = null!;
|
|
private List<ConfigNode>? _drawList = null;
|
|
private Dictionary<string, ConfigPageNode> _nestedConfigPageNodes = null!;
|
|
|
|
public PluginConfigObject ConfigObject
|
|
{
|
|
get => _configObject;
|
|
set
|
|
{
|
|
_configObject = value;
|
|
GenerateNestedConfigPageNodes();
|
|
_drawList = null;
|
|
}
|
|
}
|
|
|
|
public override List<T> GetObjects<T>()
|
|
{
|
|
return _configObject.GetObjects<T>();
|
|
}
|
|
|
|
private void GenerateNestedConfigPageNodes()
|
|
{
|
|
_nestedConfigPageNodes = new Dictionary<string, ConfigPageNode>();
|
|
|
|
FieldInfo[] fields = _configObject.GetType().GetFields();
|
|
|
|
foreach (var field in fields)
|
|
{
|
|
foreach (var attribute in field.GetCustomAttributes(true))
|
|
{
|
|
if (attribute is not NestedConfigAttribute nestedConfigAttribute)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var value = field.GetValue(_configObject);
|
|
|
|
if (value is not PluginConfigObject nestedConfig)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
ConfigPageNode configPageNode = new();
|
|
configPageNode.ConfigObject = nestedConfig;
|
|
configPageNode.Name = nestedConfigAttribute.friendlyName;
|
|
|
|
if (nestedConfig.Disableable)
|
|
{
|
|
configPageNode.Name += "##" + nestedConfig.GetHashCode();
|
|
}
|
|
|
|
_nestedConfigPageNodes.Add(field.Name, configPageNode);
|
|
}
|
|
}
|
|
}
|
|
|
|
public override string? GetBase64String()
|
|
{
|
|
if (!AllowShare())
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return ImportExportHelper.GenerateExportString(ConfigObject);
|
|
}
|
|
|
|
protected override bool AllowExport()
|
|
{
|
|
return ConfigObject.Exportable;
|
|
}
|
|
|
|
protected override bool AllowShare()
|
|
{
|
|
return ConfigObject.Shareable;
|
|
}
|
|
|
|
protected override bool AllowReset()
|
|
{
|
|
return ConfigObject.Resettable;
|
|
}
|
|
|
|
public override bool Draw(ref bool changed) { return DrawWithID(ref changed); }
|
|
|
|
private bool DrawWithID(ref bool changed, string? ID = null)
|
|
{
|
|
bool didReset = false;
|
|
|
|
// Only do this stuff the first time the config page is loaded
|
|
if (_drawList is null)
|
|
{
|
|
_drawList = GenerateDrawList();
|
|
}
|
|
|
|
if (_drawList is not null)
|
|
{
|
|
foreach (var fieldNode in _drawList)
|
|
{
|
|
didReset |= fieldNode.Draw(ref changed);
|
|
}
|
|
}
|
|
|
|
didReset |= DrawPortableSection();
|
|
|
|
ImGui.NewLine(); // fixes some long pages getting cut off
|
|
|
|
return didReset;
|
|
}
|
|
|
|
private List<ConfigNode> GenerateDrawList(string? ID = null)
|
|
{
|
|
Dictionary<string, ConfigNode> fieldMap = new Dictionary<string, ConfigNode>();
|
|
|
|
FieldInfo[] fields = ConfigObject.GetType().GetFields();
|
|
foreach (var field in fields)
|
|
{
|
|
if (ConfigObject.DisableParentSettings != null && ConfigObject.DisableParentSettings.Contains(field.Name))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (object attribute in field.GetCustomAttributes(true))
|
|
{
|
|
if (attribute is NestedConfigAttribute nestedConfigAttribute && _nestedConfigPageNodes.TryGetValue(field.Name, out ConfigPageNode? node))
|
|
{
|
|
var newNodes = node.GenerateDrawList(node.Name);
|
|
foreach (var newNode in newNodes)
|
|
{
|
|
newNode.Position = nestedConfigAttribute.pos;
|
|
newNode.Separator = nestedConfigAttribute.separator;
|
|
newNode.Spacing = nestedConfigAttribute.spacing;
|
|
newNode.ParentName = nestedConfigAttribute.collapseWith;
|
|
newNode.Nest = nestedConfigAttribute.nest;
|
|
newNode.CollapsingHeader = nestedConfigAttribute.collapsingHeader;
|
|
fieldMap.Add($"{node.Name}_{newNode.Name}", newNode);
|
|
}
|
|
}
|
|
else if (attribute is OrderAttribute orderAttribute)
|
|
{
|
|
var fieldNode = new FieldNode(field, ConfigObject, ID);
|
|
fieldNode.Position = orderAttribute.pos;
|
|
fieldNode.ParentName = orderAttribute.collapseWith;
|
|
if (fieldMap.TryGetValue(field.Name, out var existing) && existing is FieldNode existingFn &&
|
|
existingFn.Field.DeclaringType != null && field.DeclaringType != null &&
|
|
existingFn.Field.DeclaringType.IsAssignableFrom(field.DeclaringType) &&
|
|
existingFn.Field.DeclaringType != field.DeclaringType)
|
|
{
|
|
fieldMap[field.Name] = fieldNode;
|
|
}
|
|
else if (!fieldMap.ContainsKey(field.Name))
|
|
{
|
|
fieldMap.Add(field.Name, fieldNode);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var manualDrawMethods = ConfigObject.GetType().GetMethods().Where(m => Attribute.IsDefined(m, typeof(ManualDrawAttribute), false));
|
|
foreach (var method in manualDrawMethods)
|
|
{
|
|
string id = $"ManualDraw##{method.GetHashCode()}";
|
|
fieldMap.Add(id, new ManualDrawNode(method, ConfigObject, id));
|
|
}
|
|
|
|
foreach (var configNode in fieldMap.Values)
|
|
{
|
|
if (configNode.ParentName is not null &&
|
|
fieldMap.TryGetValue(configNode.ParentName, out ConfigNode? parentNode))
|
|
{
|
|
if (!ConfigObject.Disableable &&
|
|
parentNode.Name.Equals("Enabled") &&
|
|
parentNode.ID is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (parentNode is FieldNode parentFieldNode)
|
|
{
|
|
parentFieldNode.CollapseControl = true;
|
|
parentFieldNode.AddChild(configNode.Position, configNode);
|
|
}
|
|
}
|
|
}
|
|
|
|
var fieldNodes = fieldMap.Values.ToList();
|
|
fieldNodes.RemoveAll(f => f.IsChild);
|
|
fieldNodes.Sort((x, y) => x.Position - y.Position);
|
|
return fieldNodes;
|
|
}
|
|
|
|
private bool DrawPortableSection()
|
|
{
|
|
if (!AllowExport())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ImGuiHelper.DrawSeparator(2, 1);
|
|
|
|
const float buttonWidth = 120;
|
|
|
|
ImGui.BeginGroup();
|
|
|
|
float width = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
|
ImGui.SetCursorPos(new Vector2(width / 2f - buttonWidth - 5, ImGui.GetCursorPosY()));
|
|
|
|
if (ImGui.Button("Export", new Vector2(120, 24)))
|
|
{
|
|
var exportString = ImportExportHelper.GenerateExportString(ConfigObject);
|
|
ImGui.SetClipboardText(exportString);
|
|
}
|
|
|
|
ImGui.SameLine();
|
|
|
|
if (ImGui.Button("Reset", new Vector2(120, 24)))
|
|
{
|
|
_nodeToReset = this;
|
|
_nodeToResetName = Utils.UserFriendlyConfigName(ConfigObject.GetType().Name);
|
|
}
|
|
|
|
ImGui.NewLine();
|
|
ImGui.EndGroup();
|
|
|
|
return DrawResetModal();
|
|
}
|
|
|
|
public override void Save(string path)
|
|
{
|
|
string[] splits = path.Split("\\", StringSplitOptions.RemoveEmptyEntries);
|
|
string directory = path.Replace(splits.Last(), "");
|
|
Directory.CreateDirectory(directory);
|
|
|
|
string finalPath = path + ".json";
|
|
|
|
try
|
|
{
|
|
File.WriteAllText(
|
|
finalPath,
|
|
JsonConvert.SerializeObject(
|
|
ConfigObject,
|
|
Formatting.Indented,
|
|
new JsonSerializerSettings { TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, TypeNameHandling = TypeNameHandling.Objects }
|
|
)
|
|
);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Plugin.Logger.Error("Error when saving config object: " + e.Message);
|
|
}
|
|
}
|
|
|
|
public override void Load(string path)
|
|
{
|
|
if (ConfigObject is not PluginConfigObject) { return; }
|
|
|
|
FileInfo finalPath = new(path + ".json");
|
|
|
|
// Use reflection to call the LoadForType method, this allows us to specify a type at runtime.
|
|
// While in general use this is important as the conversion from the superclass 'PluginConfigObject' to a specific subclass (e.g. 'BlackMageHudConfig') would
|
|
// be handled by Json.NET, when the plugin is reloaded with a different assembly (as is the case when using LivePluginLoader, or updating the plugin in-game)
|
|
// it fails. In order to fix this we need to specify the specific subclass, in order to do this during runtime we must use reflection to set the generic.
|
|
MethodInfo? methodInfo = ConfigObject.GetType().GetMethod("Load");
|
|
MethodInfo? function = methodInfo?.MakeGenericMethod(ConfigObject.GetType());
|
|
|
|
object?[] args = new object?[] { finalPath };
|
|
PluginConfigObject? config = (PluginConfigObject?)function?.Invoke(ConfigObject, args);
|
|
|
|
ConfigObject = config ?? ConfigObject;
|
|
}
|
|
|
|
public override void Reset()
|
|
{
|
|
Type type = ConfigObject.GetType();
|
|
ImportData? importData = ProfilesManager.Instance?.DefaultImportData(type);
|
|
|
|
PluginConfigObject? config = null;
|
|
if (importData != null)
|
|
{
|
|
config = importData.GetObject();
|
|
}
|
|
|
|
if (config == null)
|
|
{
|
|
// Fallback: invoke static DefaultConfig() when no import data (e.g. new config types not yet in Default.HSUI)
|
|
MethodInfo? defaultConfigMethod = type.GetMethod("DefaultConfig", BindingFlags.Public | BindingFlags.Static);
|
|
if (defaultConfigMethod != null && defaultConfigMethod.ReturnType != typeof(void))
|
|
{
|
|
object? result = defaultConfigMethod.Invoke(null, null);
|
|
config = result as PluginConfigObject;
|
|
}
|
|
}
|
|
|
|
if (config == null)
|
|
{
|
|
Plugin.Logger.Error("Error resetting config for type " + type.ToString() + " (no import data and no DefaultConfig)");
|
|
return;
|
|
}
|
|
|
|
ConfigObject = config;
|
|
}
|
|
|
|
public override ConfigPageNode? GetOrAddConfig<T>() => this;
|
|
}
|
|
} |