Code commit

This commit is contained in:
RisaDev
2024-01-06 01:21:41 +03:00
parent a7d7297c59
commit a486dd2c96
90 changed files with 11576 additions and 0 deletions

View File

@@ -0,0 +1,539 @@
using Dalamud.Interface.Components;
using Dalamud.Interface;
using ImGuiNET;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Interface.Utility;
using OtterGui;
using OtterGui.Raii;
using CustomizePlus.Core.Data;
using CustomizePlus.Armatures.Data;
using CustomizePlus.Configuration.Data;
using CustomizePlus.Core.Helpers;
using CustomizePlus.Templates;
using CustomizePlus.Game.Services;
using CustomizePlus.Templates.Data;
namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Templates;
public class BoneEditorPanel
{
private readonly TemplateFileSystemSelector _templateFileSystemSelector;
private readonly TemplateEditorManager _editorManager;
private readonly PluginConfiguration _configuration;
private readonly GameObjectService _gameObjectService;
private BoneAttribute _editingAttribute;
private int _precision;
private bool _isShowLiveBones;
private bool _isMirrorModeEnabled;
public bool HasChanges => _editorManager.HasChanges;
public bool IsEditorActive => _editorManager.IsEditorActive;
public bool IsEditorPaused => _editorManager.IsEditorPaused;
/// <summary>
/// Was character with name from CharacterName found in the object table or not
/// </summary>
public bool IsCharacterFound { get; private set; }
public string CharacterName { get; private set; }
private ModelBone? _changedBone;
private string? _changedBoneName;
private BoneTransform? _changedBoneTransform;
private string? _newCharacterName;
private Dictionary<BoneData.BoneFamily, bool> _groupExpandedState = new();
private bool _openSavePopup;
private bool _isUnlocked = false;
public BoneEditorPanel(
TemplateFileSystemSelector templateFileSystemSelector,
TemplateEditorManager editorManager,
PluginConfiguration configuration,
GameObjectService gameObjectService)
{
_templateFileSystemSelector = templateFileSystemSelector;
_editorManager = editorManager;
_configuration = configuration;
_gameObjectService = gameObjectService;
_isShowLiveBones = configuration.EditorConfiguration.ShowLiveBones;
_isMirrorModeEnabled = configuration.EditorConfiguration.BoneMirroringEnabled;
_precision = configuration.EditorConfiguration.EditorValuesPrecision;
_editingAttribute = configuration.EditorConfiguration.EditorMode;
CharacterName = configuration.EditorConfiguration.PreviewCharacterName!;
}
public bool EnableEditor(Template template)
{
if (_editorManager.EnableEditor(template, CharacterName))
{
_editorManager.EditorProfile.LimitLookupToOwnedObjects = _configuration.EditorConfiguration.LimitLookupToOwnedObjects;
return true;
}
return false;
}
public bool DisableEditor()
{
if (!_editorManager.HasChanges)
return _editorManager.DisableEditor();
if (_editorManager.HasChanges && !IsEditorActive)
throw new Exception("Invalid state in BoneEditorPanel: has changes but editor is not active");
_openSavePopup = true;
return false;
}
public void Draw()
{
IsCharacterFound = _gameObjectService.FindActorsByName(CharacterName).Count() > 0;
_isUnlocked = IsCharacterFound && IsEditorActive && !IsEditorPaused;
if (string.IsNullOrWhiteSpace(CharacterName))
{
CharacterName = _gameObjectService.GetCurrentPlayerName();
_editorManager.ChangeEditorCharacter(CharacterName);
_configuration.EditorConfiguration.PreviewCharacterName = CharacterName;
_configuration.Save();
}
DrawEditorConfirmationPopup();
ImGui.Separator();
using (var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)))
{
using (var table = ImRaii.Table("BasicSettings", 2))
{
ImGui.TableSetupColumn("BasicCol1", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Show editor preview on").X);
ImGui.TableSetupColumn("BasicCol2", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableNextRow();
ImGuiUtil.DrawFrameColumn("Show editor preview on");
ImGui.TableNextColumn();
var width = new Vector2(ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize("Limit to my creatures").X - 68, 0);
var name = _newCharacterName ?? CharacterName;
ImGui.SetNextItemWidth(width.X);
using (var disabled = ImRaii.Disabled(!IsEditorActive || IsEditorPaused))
{
if (!_templateFileSystemSelector.IncognitoMode)
{
if (ImGui.InputText("##PreviewCharacterName", ref name, 128))
{
_newCharacterName = name;
}
if (ImGui.IsItemDeactivatedAfterEdit())
{
if (_newCharacterName == "")
_newCharacterName = _gameObjectService.GetCurrentPlayerName();
CharacterName = _newCharacterName!;
_editorManager.ChangeEditorCharacter(CharacterName);
_configuration.EditorConfiguration.PreviewCharacterName = CharacterName;
_configuration.Save();
_newCharacterName = null;
}
}
else
ImGui.TextUnformatted("Incognito active");
ImGui.SameLine();
var enabled = _editorManager.EditorProfile.LimitLookupToOwnedObjects;
if (ImGui.Checkbox("##LimitLookupToOwnedObjects", ref enabled))
{
_editorManager.EditorProfile.LimitLookupToOwnedObjects = enabled;
_configuration.EditorConfiguration.LimitLookupToOwnedObjects = enabled;
_configuration.Save();
}
ImGuiUtil.LabeledHelpMarker("Limit to my creatures",
"When enabled limits the character search to only your own summons, mounts and minions.\nUseful when there is possibility there will be another character with that name owned by another player.\n* For battle chocobo use \"Chocobo\" as character name.\n** If you are changing root scale for mount and want to keep your scale make sure your own scale is set to anything other than default value.");
}
}
using (var table = ImRaii.Table("BoneEditorMenu", 2))
{
ImGui.TableSetupColumn("Attributes", ImGuiTableColumnFlags.WidthFixed);
ImGui.TableSetupColumn("Space", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableNextRow();
ImGui.TableNextColumn();
var modeChanged = false;
if (ImGui.RadioButton("Position", _editingAttribute == BoneAttribute.Position))
{
_editingAttribute = BoneAttribute.Position;
modeChanged = true;
}
CtrlHelper.AddHoverText($"May have unintended effects. Edit at your own risk!");
ImGui.SameLine();
if (ImGui.RadioButton("Rotation", _editingAttribute == BoneAttribute.Rotation))
{
_editingAttribute = BoneAttribute.Rotation;
modeChanged = true;
}
CtrlHelper.AddHoverText($"May have unintended effects. Edit at your own risk!");
ImGui.SameLine();
if (ImGui.RadioButton("Scale", _editingAttribute == BoneAttribute.Scale))
{
_editingAttribute = BoneAttribute.Scale;
modeChanged = true;
}
if (modeChanged)
{
_configuration.EditorConfiguration.EditorMode = _editingAttribute;
_configuration.Save();
}
using (var disabled = ImRaii.Disabled(!_isUnlocked))
{
ImGui.SameLine();
if (CtrlHelper.Checkbox("Show Live Bones", ref _isShowLiveBones))
{
_configuration.EditorConfiguration.ShowLiveBones = _isShowLiveBones;
_configuration.Save();
}
CtrlHelper.AddHoverText($"If selected, present for editing all bones found in the game data,\nelse show only bones for which the profile already contains edits.");
ImGui.SameLine();
ImGui.BeginDisabled(!_isShowLiveBones);
if (CtrlHelper.Checkbox("Mirror Mode", ref _isMirrorModeEnabled))
{
_configuration.EditorConfiguration.BoneMirroringEnabled = _isMirrorModeEnabled;
_configuration.Save();
}
CtrlHelper.AddHoverText($"Bone changes will be reflected from left to right and vice versa");
ImGui.EndDisabled();
}
ImGui.TableNextColumn();
if (ImGui.SliderInt("##Precision", ref _precision, 0, 6, $"{_precision} Place{(_precision == 1 ? "" : "s")}"))
{
_configuration.EditorConfiguration.EditorValuesPrecision = _precision;
_configuration.Save();
}
CtrlHelper.AddHoverText("Level of precision to display while editing values");
}
ImGui.Separator();
using (var table = ImRaii.Table("BoneEditorContents", 6, ImGuiTableFlags.BordersOuterH | ImGuiTableFlags.BordersV | ImGuiTableFlags.ScrollY))
{
if (!table)
return;
var col1Label = _editingAttribute == BoneAttribute.Rotation ? "Roll" : "X";
var col2Label = _editingAttribute == BoneAttribute.Rotation ? "Pitch" : "Y";
var col3Label = _editingAttribute == BoneAttribute.Rotation ? "Yaw" : "Z";
var col4Label = _editingAttribute == BoneAttribute.Scale ? "All" : "N/A";
ImGui.TableSetupColumn("Bones", ImGuiTableColumnFlags.NoReorder | ImGuiTableColumnFlags.WidthFixed, 3 * CtrlHelper.IconButtonWidth);
ImGui.TableSetupColumn($"{col1Label}", ImGuiTableColumnFlags.NoReorder | ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn($"{col2Label}", ImGuiTableColumnFlags.NoReorder | ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn($"{col3Label}", ImGuiTableColumnFlags.NoReorder | ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn($"{col4Label}", ImGuiTableColumnFlags.NoReorder | ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetColumnEnabled(4, _editingAttribute == BoneAttribute.Scale);
ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.NoReorder | ImGuiTableColumnFlags.WidthStretch);
ImGui.TableHeadersRow();
IEnumerable<EditRowParams> relevantModelBones = null!;
if (_editorManager.IsEditorActive && _editorManager.EditorProfile != null && _editorManager.EditorProfile.Armatures.Count > 0)
relevantModelBones = _isShowLiveBones && _editorManager.EditorProfile.Armatures.Count > 0
? _editorManager.EditorProfile.Armatures[0].GetAllBones().DistinctBy(x => x.BoneName).Select(x => new EditRowParams(x))
: _editorManager.EditorProfile.Armatures[0].BoneTemplateBinding.Where(x => x.Value.Bones.ContainsKey(x.Key))
.Select(x => new EditRowParams(x.Key, x.Value.Bones[x.Key])); //todo: this is awful
else
relevantModelBones = _templateFileSystemSelector.Selected!.Bones.Select(x => new EditRowParams(x.Key, x.Value));
var groupedBones = relevantModelBones.GroupBy(x => BoneData.GetBoneFamily(x.BoneCodeName));
foreach (var boneGroup in groupedBones.OrderBy(x => (int)x.Key))
{
//Hide root bone if it's not enabled in settings or if we are in rotation mode
if (boneGroup.Key == BoneData.BoneFamily.Root &&
(!_configuration.EditorConfiguration.RootPositionEditingEnabled ||
_editingAttribute == BoneAttribute.Rotation))
continue;
//create a dropdown entry for the family if one doesn't already exist
//mind that it'll only be rendered if bones exist to fill it
if (!_groupExpandedState.TryGetValue(boneGroup.Key, out var expanded))
{
_groupExpandedState[boneGroup.Key] = false;
expanded = false;
}
if (expanded)
{
//paint the row in header colors if it's expanded
ImGui.TableNextRow(ImGuiTableRowFlags.Headers);
}
else
{
ImGui.TableNextRow();
}
using var id = ImRaii.PushId(boneGroup.Key.ToString());
ImGui.TableNextColumn();
CtrlHelper.ArrowToggle($"##{boneGroup.Key}", ref expanded);
ImGui.SameLine();
CtrlHelper.StaticLabel(boneGroup.Key.ToString());
if (BoneData.DisplayableFamilies.TryGetValue(boneGroup.Key, out var tip) && tip != null)
CtrlHelper.AddHoverText(tip);
if (expanded)
{
ImGui.TableNextRow();
foreach (var erp in boneGroup.OrderBy(x => BoneData.GetBoneRanking(x.BoneCodeName)))
{
CompleteBoneEditor(erp);
}
}
_groupExpandedState[boneGroup.Key] = expanded;
}
}
}
}
private void DrawEditorConfirmationPopup()
{
if (_openSavePopup)
{
ImGui.OpenPopup("SavePopup");
_openSavePopup = false;
}
var viewportSize = ImGui.GetWindowViewport().Size;
ImGui.SetNextWindowSize(new Vector2(viewportSize.X / 4, viewportSize.Y / 12));
ImGui.SetNextWindowPos(viewportSize / 2, ImGuiCond.Always, new Vector2(0.5f));
using var popup = ImRaii.Popup("SavePopup", ImGuiWindowFlags.Modal);
if (!popup)
return;
ImGui.SetCursorPos(new Vector2(ImGui.GetWindowWidth() / 4 - 40, ImGui.GetWindowHeight() / 4));
ImGuiUtil.TextWrapped("You have unsaved changes in current template, what would you like to do?");
var buttonWidth = new Vector2(150 * ImGuiHelpers.GlobalScale, 0);
var yPos = ImGui.GetWindowHeight() - 2 * ImGui.GetFrameHeight();
var xPos = (ImGui.GetWindowWidth() - ImGui.GetStyle().ItemSpacing.X) / 4 - buttonWidth.X;
ImGui.SetCursorPos(new Vector2(xPos, yPos));
if (ImGui.Button("Save", buttonWidth))
{
_editorManager.SaveChanges();
_editorManager.DisableEditor();
ImGui.CloseCurrentPopup();
}
ImGui.SameLine();
if (ImGui.Button("Save as a copy", buttonWidth))
{
_editorManager.SaveChanges(true);
_editorManager.DisableEditor();
ImGui.CloseCurrentPopup();
}
ImGui.SameLine();
if (ImGui.Button("Do not save", buttonWidth))
{
_editorManager.DisableEditor();
ImGui.CloseCurrentPopup();
}
ImGui.SameLine();
if (ImGui.Button("Keep editing", buttonWidth))
{
ImGui.CloseCurrentPopup();
}
}
#region ImGui helper functions
public bool ResetBoneButton(string codename)
{
var output = ImGuiComponents.IconButton(codename, FontAwesomeIcon.Recycle);
CtrlHelper.AddHoverText(
$"Reset '{BoneData.GetBoneDisplayName(codename)}' to default {_editingAttribute} values");
if (output)
_editorManager.ResetBoneAttributeChanges(codename, _editingAttribute);
return output;
}
private bool RevertBoneButton(string codename)
{
var output = ImGuiComponents.IconButton(codename, FontAwesomeIcon.ArrowCircleLeft);
CtrlHelper.AddHoverText(
$"Revert '{BoneData.GetBoneDisplayName(codename)}' to last saved {_editingAttribute} values");
if (output)
_editorManager.RevertBoneAttributeChanges(codename, _editingAttribute);
return output;
}
private bool FullBoneSlider(string label, ref Vector3 value)
{
var velocity = _editingAttribute == BoneAttribute.Rotation ? 0.1f : 0.001f;
var minValue = _editingAttribute == BoneAttribute.Rotation ? -360.0f : -10.0f;
var maxValue = _editingAttribute == BoneAttribute.Rotation ? 360.0f : 10.0f;
var temp = _editingAttribute switch
{
BoneAttribute.Position => 0.0f,
BoneAttribute.Rotation => 0.0f,
_ => value.X == value.Y && value.Y == value.Z ? value.X : 1.0f
};
ImGui.PushItemWidth(ImGui.GetColumnWidth());
if (ImGui.DragFloat(label, ref temp, velocity, minValue, maxValue, $"%.{_precision}f"))
{
value = new Vector3(temp, temp, temp);
return true;
}
return false;
}
private bool SingleValueSlider(string label, ref float value)
{
var velocity = _editingAttribute == BoneAttribute.Rotation ? 0.1f : 0.001f;
var minValue = _editingAttribute == BoneAttribute.Rotation ? -360.0f : -10.0f;
var maxValue = _editingAttribute == BoneAttribute.Rotation ? 360.0f : 10.0f;
ImGui.PushItemWidth(ImGui.GetColumnWidth());
var temp = value;
if (ImGui.DragFloat(label, ref temp, velocity, minValue, maxValue, $"%.{_precision}f"))
{
value = temp;
return true;
}
return false;
}
private void CompleteBoneEditor(EditRowParams bone)
{
var codename = bone.BoneCodeName;
var displayName = bone.BoneDisplayName;
var transform = new BoneTransform(bone.Transform);
var flagUpdate = false;
var newVector = _editingAttribute switch
{
BoneAttribute.Position => transform.Translation,
BoneAttribute.Rotation => transform.Rotation,
_ => transform.Scaling
};
using var id = ImRaii.PushId(codename);
ImGui.TableNextColumn();
using (var disabled = ImRaii.Disabled(!_isUnlocked))
{
//----------------------------------
ImGui.Dummy(new Vector2(CtrlHelper.IconButtonWidth * 0.75f, 0));
ImGui.SameLine();
ResetBoneButton(codename);
ImGui.SameLine();
RevertBoneButton(codename);
//----------------------------------
ImGui.TableNextColumn();
flagUpdate |= SingleValueSlider($"##{displayName}-X", ref newVector.X);
//----------------------------------
ImGui.TableNextColumn();
flagUpdate |= SingleValueSlider($"##{displayName}-Y", ref newVector.Y);
//-----------------------------------
ImGui.TableNextColumn();
flagUpdate |= SingleValueSlider($"##{displayName}-Z", ref newVector.Z);
//----------------------------------
if (_editingAttribute != BoneAttribute.Scale)
ImGui.BeginDisabled();
ImGui.TableNextColumn();
var tempVec = new Vector3(newVector.X, newVector.Y, newVector.Z);
flagUpdate |= FullBoneSlider($"##{displayName}-All", ref newVector);
if (_editingAttribute != BoneAttribute.Scale)
ImGui.EndDisabled();
}
//----------------------------------
ImGui.TableNextColumn();
CtrlHelper.StaticLabel(displayName, CtrlHelper.TextAlignment.Left, BoneData.IsIVCSBone(codename) ? $"(IVCS) {codename}" : codename);
if (flagUpdate)
{
transform.UpdateAttribute(_editingAttribute, newVector);
_editorManager.ModifyBoneTransform(codename, transform);
if (_isMirrorModeEnabled && bone.Basis?.TwinBone != null) //todo: put it inside manager
_editorManager.ModifyBoneTransform(bone.Basis.TwinBone.BoneName,
BoneData.IsIVCSBone(codename) ? transform.GetSpecialReflection() : transform.GetStandardReflection());
}
ImGui.TableNextRow();
}
#endregion
}
/// <summary>
/// Simple structure for representing arguments to the editor table.
/// Can be constructed with or without access to a live armature.
/// </summary>
internal struct EditRowParams
{
public string BoneCodeName;
public string BoneDisplayName => BoneData.GetBoneDisplayName(BoneCodeName);
public BoneTransform Transform;
public ModelBone? Basis = null;
public EditRowParams(ModelBone mb)
{
BoneCodeName = mb.BoneName;
Transform = mb.CustomizedTransform ?? new BoneTransform();
Basis = mb;
}
public EditRowParams(string codename, BoneTransform tr)
{
BoneCodeName = codename;
Transform = tr;
Basis = null;
}
}

View File

@@ -0,0 +1,408 @@
using Dalamud.Interface;
using Dalamud.Plugin.Services;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Filesystem;
using OtterGui.FileSystem.Selector;
using OtterGui.Log;
using OtterGui.Raii;
using System;
using System.Numerics;
using static CustomizePlus.UI.Windows.MainWindow.Tabs.Templates.TemplateFileSystemSelector;
using Newtonsoft.Json;
using System.Windows.Forms;
using System.Linq;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.ImGuiFileDialog;
using System.IO;
using System.Reflection;
using CustomizePlus.Templates;
using CustomizePlus.Configuration.Data;
using CustomizePlus.Profiles;
using CustomizePlus.Core.Helpers;
using CustomizePlus.Anamnesis;
using CustomizePlus.Profiles.Data;
using CustomizePlus.Templates.Events;
using CustomizePlus.Profiles.Events;
using CustomizePlus.Templates.Data;
namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Templates;
public class TemplateFileSystemSelector : FileSystemSelector<Template, TemplateState>
{
private readonly PluginConfiguration _configuration;
private readonly TemplateEditorManager _editorManager;
private readonly TemplateManager _templateManager;
private readonly TemplateChanged _templateChangedEvent;
private readonly ProfileChanged _profileChangedEvent;
private readonly ProfileManager _profileManager;
private readonly MessageService _messageService;
private readonly PoseFileBoneLoader _poseFileBoneLoader;
private readonly Logger _logger;
private readonly PopupSystem _popupSystem;
private readonly FileDialogManager _importFilePicker = new();
private string? _clipboardText;
private Template? _cloneTemplate;
private string _newName = string.Empty;
public bool IncognitoMode
{
get => _configuration.UISettings.IncognitoMode;
set
{
_configuration.UISettings.IncognitoMode = value;
_configuration.Save();
}
}
public struct TemplateState
{
public ColorId Color;
}
public TemplateFileSystemSelector(
TemplateFileSystem fileSystem,
IKeyState keyState,
Logger logger,
PluginConfiguration configuration,
TemplateEditorManager editorManager,
TemplateManager templateManager,
TemplateChanged templateChangedEvent,
ProfileChanged profileChangedEvent,
ProfileManager profileManager,
MessageService messageService,
PoseFileBoneLoader poseFileBoneLoader,
PopupSystem popupSystem)
: base(fileSystem, keyState, logger, allowMultipleSelection: true)
{
_configuration = configuration;
_editorManager = editorManager;
_templateManager = templateManager;
_templateChangedEvent = templateChangedEvent;
_profileChangedEvent = profileChangedEvent;
_profileManager = profileManager;
_messageService = messageService;
_poseFileBoneLoader = poseFileBoneLoader;
_logger = logger;
_popupSystem = popupSystem;
_popupSystem.RegisterPopup("template_editor_active_warn", "You need to stop bone editing before doing this action"/*, false, new Vector2(5, 12)*/);
_templateChangedEvent.Subscribe(OnTemplateChange, TemplateChanged.Priority.TemplateFileSystemSelector);
_profileChangedEvent.Subscribe(OnProfileChange, ProfileChanged.Priority.TemplateFileSystemSelector);
AddButton(NewButton, 0);
AddButton(AnamnesisImportButton, 10);
AddButton(ClipboardImportButton, 20);
AddButton(CloneButton, 30);
AddButton(DeleteButton, 1000);
SetFilterTooltip();
}
public void Dispose()
{
base.Dispose();
_templateChangedEvent.Unsubscribe(OnTemplateChange);
_profileChangedEvent.Unsubscribe(OnProfileChange);
}
protected override uint ExpandedFolderColor
=> ColorId.FolderExpanded.Value();
protected override uint CollapsedFolderColor
=> ColorId.FolderCollapsed.Value();
protected override uint FolderLineColor
=> ColorId.FolderLine.Value();
protected override bool FoldersDefaultOpen
=> _configuration.UISettings.FoldersDefaultOpen;
protected override void DrawLeafName(FileSystem<Template>.Leaf leaf, in TemplateState state, bool selected)
{
var flag = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags;
var name = IncognitoMode ? leaf.Value.Incognito : leaf.Value.Name.Text;
using var color = ImRaii.PushColor(ImGuiCol.Text, state.Color.Value());
using var _ = ImRaii.TreeNode(name, flag);
}
protected override void Select(FileSystem<Template>.Leaf? leaf, bool clear, in TemplateState storage = default)
{
if (_editorManager.IsEditorActive)
{
Plugin.Logger.Debug("Blocked edited item change");
ShowEditorWarningPopup();
return;
}
base.Select(leaf, clear, storage);
}
protected override void DrawPopups()
{
_importFilePicker.Draw();
//DrawEditorWarningPopup();
DrawNewTemplatePopup();
}
private void ShowEditorWarningPopup()
{
_popupSystem.ShowPopup("template_editor_active_warn");
}
private void DrawNewTemplatePopup()
{
if (!ImGuiUtil.OpenNameField("##NewTemplate", ref _newName))
return;
if (_clipboardText != null)
{
var importVer = Base64Helper.ImportFromBase64(Clipboard.GetText(), out var json);
var template = Convert.ToInt32(importVer) switch
{
//0 => ProfileConverter.ConvertFromConfigV0(json),
//2 => ProfileConverter.ConvertFromConfigV2(json),
//3 =>
4 => JsonConvert.DeserializeObject<Template>(json),
_ => null
};
if (template is Template tpl)
_templateManager.Clone(tpl, _newName, true);
else
//Messager.NotificationMessage("Could not create a template, clipboard did not contain valid template data.", NotificationType.Error, false);
throw new Exception("Invalid template"); //todo: temporary
_clipboardText = null;
}
else if (_cloneTemplate != null)
{
_templateManager.Clone(_cloneTemplate, _newName, true);
_cloneTemplate = null;
}
else
{
_templateManager.Create(_newName, true);
}
_newName = string.Empty;
}
private void OnTemplateChange(TemplateChanged.Type type, Template? nullable, object? arg3 = null)
{
switch (type)
{
case TemplateChanged.Type.Created:
case TemplateChanged.Type.Deleted:
case TemplateChanged.Type.Renamed:
case TemplateChanged.Type.ReloadedAll:
SetFilterDirty();
break;
}
}
private void OnProfileChange(ProfileChanged.Type type, Profile? profile, object? arg3 = null)
{
switch (type)
{
case ProfileChanged.Type.Created:
case ProfileChanged.Type.Deleted:
case ProfileChanged.Type.AddedTemplate:
case ProfileChanged.Type.ChangedTemplate:
case ProfileChanged.Type.RemovedTemplate:
case ProfileChanged.Type.ReloadedAll:
SetFilterDirty();
break;
}
}
private void NewButton(Vector2 size)
{
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), size, "Create a new template with default configuration.", false,
true))
return;
if (_editorManager.IsEditorActive)
{
ShowEditorWarningPopup();
return;
}
ImGui.OpenPopup("##NewTemplate");
}
private void ClipboardImportButton(Vector2 size)
{
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), size, "Try to import a template from your clipboard.", false,
true))
return;
if (_editorManager.IsEditorActive)
{
ShowEditorWarningPopup();
return;
}
try
{
_clipboardText = ImGui.GetClipboardText();
ImGui.OpenPopup("##NewTemplate");
}
catch
{
_messageService.NotificationMessage("Could not import data from clipboard.", NotificationType.Error, false);
}
}
private void AnamnesisImportButton(Vector2 size)
{
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), size, "Import a template from anamnesis pose file (scaling only)", false,
true))
return;
if (_editorManager.IsEditorActive)
{
ShowEditorWarningPopup();
return;
}
_importFilePicker.OpenFileDialog("Import Pose File", ".pose", (isSuccess, path) =>
{
if (isSuccess)
{
var selectedFilePath = path.FirstOrDefault();
//todo: check for selectedFilePath == null?
var bones = _poseFileBoneLoader.LoadBoneTransformsFromFile(selectedFilePath);
if (bones != null)
{
if (bones.Count == 0)
{
_messageService.NotificationMessage("Selected anamnesis pose file doesn't contain any scaled bones", NotificationType.Error);
return;
}
_templateManager.Create(Path.GetFileNameWithoutExtension(selectedFilePath), bones, false);
}
else
{
_messageService.NotificationMessage(
$"Error parsing anamnesis pose file at '{path}'", NotificationType.Error);
}
}
else
{
_logger.Debug(isSuccess + " NO valid file has been selected. " + path);
}
}, 1, null, true);
/*MessageDialog.Show(
"Due to technical limitations, Customize+ is only able to import scale values from *.pose files.\nPosition and rotation information will be ignored.",
new Vector2(570, 100), ImportAction, "ana_import_pos_rot_warning");*/
//todo: message dialog?
}
private void CloneButton(Vector2 size)
{
var tt = SelectedLeaf == null
? "No template selected."
: "Clone the currently selected template to a duplicate";
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clone.ToIconString(), size, tt, SelectedLeaf == null, true))
return;
if (_editorManager.IsEditorActive)
{
ShowEditorWarningPopup();
return;
}
_cloneTemplate = Selected!;
ImGui.OpenPopup("##NewTemplate");
}
private void DeleteButton(Vector2 size)
=> DeleteSelectionButton(size, _configuration.UISettings.DeleteTemplateModifier, "template", "templates", (template) =>
{
if (_editorManager.IsEditorActive)
{
ShowEditorWarningPopup();
return;
}
_templateManager.Delete(template);
});
#region Filters
private const StringComparison IgnoreCase = StringComparison.OrdinalIgnoreCase;
private LowerString _filter = LowerString.Empty;
private int _filterType = -1;
private void SetFilterTooltip()
{
FilterTooltip = "Filter templates for those where their full paths or names contain the given substring.\n"
+ "Enter n:[string] to filter only for template names and no paths.";
}
/// <summary> Appropriately identify and set the string filter and its type. </summary>
protected override bool ChangeFilter(string filterValue)
{
(_filter, _filterType) = filterValue.Length switch
{
0 => (LowerString.Empty, -1),
> 1 when filterValue[1] == ':' =>
filterValue[0] switch
{
'n' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 1),
'N' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 1),
_ => (new LowerString(filterValue), 0),
},
_ => (new LowerString(filterValue), 0),
};
return true;
}
/// <summary>
/// The overwritten filter method also computes the state.
/// Folders have default state and are filtered out on the direct string instead of the other options.
/// If any filter is set, they should be hidden by default unless their children are visible,
/// or they contain the path search string.
/// </summary>
protected override bool ApplyFiltersAndState(FileSystem<Template>.IPath path, out TemplateState state)
{
if (path is TemplateFileSystem.Folder f)
{
state = default;
return FilterValue.Length > 0 && !f.FullName().Contains(FilterValue, IgnoreCase);
}
return ApplyFiltersAndState((TemplateFileSystem.Leaf)path, out state);
}
/// <summary> Apply the string filters. </summary>
private bool ApplyStringFilters(TemplateFileSystem.Leaf leaf, Template template)
{
return _filterType switch
{
-1 => false,
0 => !(_filter.IsContained(leaf.FullName()) || template.Name.Contains(_filter)),
1 => !template.Name.Contains(_filter),
_ => false, // Should never happen
};
}
/// <summary> Combined wrapper for handling all filters and setting state. </summary>
private bool ApplyFiltersAndState(TemplateFileSystem.Leaf leaf, out TemplateState state)
{
//todo: more efficient to store links to profiles in templates than iterating here
state.Color = _profileManager.GetProfilesUsingTemplate(leaf.Value).Any() ? ColorId.UsedTemplate : ColorId.UnusedTemplate;
return ApplyStringFilters(leaf, leaf.Value);
}
#endregion
}

View File

@@ -0,0 +1,229 @@
using Dalamud.Interface;
using Dalamud.Interface.Internal.Notifications;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using System;
using System.Linq;
using System.Numerics;
using System.Windows.Forms;
using CustomizePlus.Core.Data;
using CustomizePlus.Game.Services;
using CustomizePlus.Templates;
using CustomizePlus.Configuration.Data;
using CustomizePlus.Core.Helpers;
using CustomizePlus.Templates.Data;
namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Templates;
public class TemplatePanel
{
private readonly TemplateFileSystemSelector _selector;
private readonly TemplateManager _manager;
private readonly GameStateService _gameStateService;
private readonly BoneEditorPanel _boneEditor;
private readonly PluginConfiguration _configuration;
private readonly MessageService _messageService;
private string? _newName;
private Template? _changedTemplate;
private string SelectionName
=> _selector.Selected == null ? "No Selection" : _selector.IncognitoMode ? _selector.Selected.Incognito : _selector.Selected.Name.Text;
public TemplatePanel(
TemplateFileSystemSelector selector,
TemplateManager manager,
GameStateService gameStateService,
BoneEditorPanel boneEditor,
PluginConfiguration configuration,
MessageService messageService)
{
_selector = selector;
_manager = manager;
_gameStateService = gameStateService;
_boneEditor = boneEditor;
_configuration = configuration;
_messageService = messageService;
}
public void Draw()
{
using var group = ImRaii.Group();
if (_selector.SelectedPaths.Count > 1)
{
DrawMultiSelection();
}
else
{
DrawHeader();
DrawPanel();
}
}
private HeaderDrawer.Button LockButton()
=> _selector.Selected == null
? HeaderDrawer.Button.Invisible
: _selector.Selected.IsWriteProtected
? new HeaderDrawer.Button
{
Description = "Make this template editable.",
Icon = FontAwesomeIcon.Lock,
OnClick = () => _manager.SetWriteProtection(_selector.Selected!, false),
Disabled = _boneEditor.IsEditorActive
}
: new HeaderDrawer.Button
{
Description = "Write-protect this template.",
Icon = FontAwesomeIcon.LockOpen,
OnClick = () => _manager.SetWriteProtection(_selector.Selected!, true),
Disabled = _boneEditor.IsEditorActive
};
/*private HeaderDrawer.Button SetFromClipboardButton()
=> new()
{
Description =
"Try to apply a template from your clipboard over this template.",
Icon = FontAwesomeIcon.Clipboard,
OnClick = SetFromClipboard,
Visible = _selector.Selected != null,
Disabled = (_selector.Selected?.IsWriteProtected ?? true) || _boneEditor.IsEditorActive,
};*/
private HeaderDrawer.Button ExportToClipboardButton()
=> new()
{
Description = "Copy the current template to your clipboard.",
Icon = FontAwesomeIcon.Copy,
OnClick = ExportToClipboard,
Visible = _selector.Selected != null,
Disabled = _boneEditor.IsEditorActive
};
private void DrawHeader()
=> HeaderDrawer.Draw(SelectionName, 0, ImGui.GetColorU32(ImGuiCol.FrameBg),
1, /*SetFromClipboardButton(),*/ ExportToClipboardButton(), LockButton(),
HeaderDrawer.Button.IncognitoButton(_selector.IncognitoMode, v => _selector.IncognitoMode = v));
private void DrawMultiSelection()
{
if (_selector.SelectedPaths.Count == 0)
return;
var sizeType = ImGui.GetFrameHeight();
var availableSizePercent = (ImGui.GetContentRegionAvail().X - sizeType - 4 * ImGui.GetStyle().CellPadding.X) / 100;
var sizeMods = availableSizePercent * 35;
var sizeFolders = availableSizePercent * 65;
ImGui.NewLine();
ImGui.TextUnformatted("Currently Selected Templates");
ImGui.Separator();
using var table = ImRaii.Table("templates", 3, ImGuiTableFlags.RowBg);
ImGui.TableSetupColumn("btn", ImGuiTableColumnFlags.WidthFixed, sizeType);
ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthFixed, sizeMods);
ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthFixed, sizeFolders);
var i = 0;
foreach (var (fullName, path) in _selector.SelectedPaths.Select(p => (p.FullName(), p))
.OrderBy(p => p.Item1, StringComparer.OrdinalIgnoreCase))
{
using var id = ImRaii.PushId(i++);
ImGui.TableNextColumn();
var icon = (path is TemplateFileSystem.Leaf ? FontAwesomeIcon.FileCircleMinus : FontAwesomeIcon.FolderMinus).ToIconString();
if (ImGuiUtil.DrawDisabledButton(icon, new Vector2(sizeType), "Remove from selection.", false, true))
_selector.RemovePathFromMultiSelection(path);
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(path is TemplateFileSystem.Leaf l ? _selector.IncognitoMode ? l.Value.Incognito : l.Value.Name.Text : string.Empty);
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(_selector.IncognitoMode ? "Incognito is active" : fullName);
}
}
private void DrawPanel()
{
using var child = ImRaii.Child("##Panel", -Vector2.One, true);
if (!child || _selector.Selected == null)
return;
using (var disabled = ImRaii.Disabled(_selector.Selected?.IsWriteProtected ?? true))
{
DrawBasicSettings();
DrawEditorToggle();
}
_boneEditor.Draw();
}
private void DrawEditorToggle()
{
if (ImGuiUtil.DrawDisabledButton($"{(_boneEditor.IsEditorActive ? "Finish" : "Start")} bone editing", Vector2.Zero,
"Toggle the bone editor for this template",
(_selector.Selected?.IsWriteProtected ?? true) || _gameStateService.GameInPosingMode() || !_configuration.PluginEnabled))
{
if (!_boneEditor.IsEditorActive)
_boneEditor.EnableEditor(_selector.Selected!);
else
_boneEditor.DisableEditor();
}
}
private void DrawBasicSettings()
{
using (var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)))
{
using (var table = ImRaii.Table("BasicSettings", 2))
{
ImGui.TableSetupColumn("BasicCol1", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("lorem ipsum dolor").X);
ImGui.TableSetupColumn("BasicCol2", ImGuiTableColumnFlags.WidthStretch);
ImGuiUtil.DrawFrameColumn("Template Name");
ImGui.TableNextColumn();
var width = new Vector2(ImGui.GetContentRegionAvail().X, 0);
var name = _newName ?? _selector.Selected!.Name;
ImGui.SetNextItemWidth(width.X);
if (!_selector.IncognitoMode)
{
if (ImGui.InputText("##Name", ref name, 128))
{
_newName = name;
_changedTemplate = _selector.Selected;
}
if (ImGui.IsItemDeactivatedAfterEdit() && _changedTemplate != null)
{
_manager.Rename(_changedTemplate, name);
_newName = null;
_changedTemplate = null;
}
}
else
ImGui.TextUnformatted(_selector.Selected!.Incognito);
}
}
}
/*private void SetFromClipboard()
{
}*/
private void ExportToClipboard()
{
try
{
Clipboard.SetText(Base64Helper.ExportToBase64(_selector.Selected!, Constants.ConfigurationVersion));
}
catch (Exception ex)
{
_messageService.NotificationMessage(ex, $"Could not copy {_selector.Selected!.Name} data to clipboard.",
$"Could not copy data from template {_selector.Selected!.UniqueId} to clipboard", NotificationType.Error, false);
}
}
}

View File

@@ -0,0 +1,23 @@
using Dalamud.Interface.Utility;
using ImGuiNET;
namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Templates;
public class TemplatesTab
{
private readonly TemplateFileSystemSelector _selector;
private readonly TemplatePanel _panel;
public TemplatesTab(TemplateFileSystemSelector selector, TemplatePanel panel)
{
_selector = selector;
_panel = panel;
}
public void Draw()
{
_selector.Draw(200f * ImGuiHelpers.GlobalScale);
ImGui.SameLine();
_panel.Draw();
}
}