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,152 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using Dalamud.Plugin.Services;
using ImGuiNET;
using Newtonsoft.Json;
using OtterGui.Raii;
using System.Linq;
using CustomizePlus.Profiles;
using CustomizePlus.Configuration.Helpers;
using CustomizePlus.Game.Services;
using CustomizePlus.GameData.Services;
namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Debug;
public class IPCTestTab //: IDisposable
{
private readonly IObjectTable _objectTable;
private readonly ProfileManager _profileManager;
private readonly PopupSystem _popupSystem;
private readonly GameObjectService _gameObjectService;
private readonly ObjectManager _objectManager;
private readonly ActorService _actorService;
private readonly ICallGateSubscriber<(int, int)>? _getApiVersion;
private readonly ICallGateSubscriber<string, Character?, object>? _setCharacterProfile;
private readonly ICallGateSubscriber<Character?, string>? _getProfileFromCharacter;
private readonly ICallGateSubscriber<Character?, object>? _revertCharacter;
//private readonly ICallGateSubscriber<string?, string?, object?>? _onProfileUpdate;
private string? _rememberedProfileJson;
private (int, int) _apiVersion;
private string? _targetCharacterName;
public IPCTestTab(
DalamudPluginInterface pluginInterface,
IObjectTable objectTable,
ProfileManager profileManager,
PopupSystem popupSystem,
ObjectManager objectManager,
GameObjectService gameObjectService,
ActorService actorService)
{
_objectTable = objectTable;
_profileManager = profileManager;
_popupSystem = popupSystem;
_objectManager = objectManager;
_gameObjectService = gameObjectService;
_actorService = actorService;
_popupSystem.RegisterPopup("ipc_v4_profile_remembered", "Current profile has been copied into memory");
_popupSystem.RegisterPopup("ipc_get_profile_from_character_remembered", "GetProfileFromCharacter result has been copied into memory");
_popupSystem.RegisterPopup("ipc_set_profile_to_character_done", "SetProfileToCharacter has been called with data from memory");
_popupSystem.RegisterPopup("ipc_revert_done", "Revert has been called");
_getApiVersion = pluginInterface.GetIpcSubscriber<(int, int)>("CustomizePlus.GetApiVersion");
_apiVersion = _getApiVersion.InvokeFunc();
_setCharacterProfile = pluginInterface.GetIpcSubscriber<string, Character?, object>("CustomizePlus.SetProfileToCharacter");
_getProfileFromCharacter = pluginInterface.GetIpcSubscriber<Character?, string>("CustomizePlus.GetProfileFromCharacter");
_revertCharacter = pluginInterface.GetIpcSubscriber<Character?, object>("CustomizePlus.RevertCharacter");
/*_onProfileUpdate = pluginInterface.GetIpcSubscriber<string?, string?, object?>("CustomizePlus.OnProfileUpdate");
_onProfileUpdate.Subscribe(OnProfileUpdate);*/
}
/* public void Dispose()
{
_onProfileUpdate?.Unsubscribe(OnProfileUpdate);
}
private void OnProfileUpdate(string? characterName, string? profileJson)
{
_lastProfileUpdate = DateTime.Now;
_lastProfileUpdateName = characterName;
}
*/
public unsafe void Draw()
{
_objectManager.Update();
if (_targetCharacterName == null)
_targetCharacterName = _gameObjectService.GetCurrentPlayerName();
ImGui.Text($"Version: {_apiVersion.Item1}.{_apiVersion.Item2}");
//ImGui.Text($"Last profile update: {_lastProfileUpdate}, Character: {_lastProfileUpdateName}");
ImGui.Text($"Memory: {(string.IsNullOrWhiteSpace(_rememberedProfileJson) ? "empty" : "has data")}");
ImGui.Text("Character to operate on:");
ImGui.SameLine();
ImGui.InputText("##operateon", ref _targetCharacterName, 128);
if (ImGui.Button("Copy current profile into memory as V3"))
{
var actors = _gameObjectService.FindActorsByName(_targetCharacterName).ToList();
if (actors.Count == 0)
return;
if (!actors[0].Item2.Identifier(_actorService.AwaitedService, out var identifier))
return;
var profile = _profileManager.GetEnabledProfilesByActor(identifier).FirstOrDefault();
if (profile == null)
return;
_rememberedProfileJson = JsonConvert.SerializeObject(V4ProfileToV3Converter.Convert(profile));
_popupSystem.ShowPopup("ipc_v4_profile_remembered");
}
if (ImGui.Button("GetProfileFromCharacter into memory"))
{
var actors = _gameObjectService.FindActorsByName(_targetCharacterName).ToList();
if (actors.Count == 0)
return;
_rememberedProfileJson = _getProfileFromCharacter!.InvokeFunc(FindCharacterByAddress(actors[0].Item2.Address));
_popupSystem.ShowPopup("ipc_get_profile_from_character_remembered");
}
using (var disabled = ImRaii.Disabled(_rememberedProfileJson == null))
{
if (ImGui.Button("SetProfileToCharacter from memory") && _rememberedProfileJson != null)
{
var actors = _gameObjectService.FindActorsByName(_targetCharacterName).ToList();
if (actors.Count == 0)
return;
_setCharacterProfile!.InvokeAction(_rememberedProfileJson, FindCharacterByAddress(actors[0].Item2.Address));
_popupSystem.ShowPopup("ipc_set_profile_to_character_done");
}
}
if (ImGui.Button("RevertCharacter") && _rememberedProfileJson != null)
{
var actors = _gameObjectService.FindActorsByName(_targetCharacterName).ToList();
if (actors.Count == 0)
return;
_revertCharacter!.InvokeAction(FindCharacterByAddress(actors[0].Item2.Address));
_popupSystem.ShowPopup("ipc_revert_done");
}
}
private Character? FindCharacterByAddress(nint address)
{
foreach (var obj in _objectTable)
if (obj.Address == address)
return (Character)obj;
return null;
}
}

View File

@@ -0,0 +1,195 @@
using ImGuiNET;
using System.Linq;
using System;
using CustomizePlus.Armatures.Data;
using CustomizePlus.Profiles;
using CustomizePlus.Armatures.Services;
using CustomizePlus.Templates;
using CustomizePlus.Profiles.Data;
using CustomizePlus.Templates.Data;
using CustomizePlus.GameData.Extensions;
using CustomizePlus.GameData.Services;
namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Debug;
public class StateMonitoringTab
{
private readonly ProfileManager _profileManager;
private readonly TemplateManager _templateManager;
private readonly ArmatureManager _armatureManager;
private readonly ObjectManager _objectManager;
public StateMonitoringTab(
ProfileManager profileManager,
TemplateManager templateManager,
ArmatureManager armatureManager,
ObjectManager objectManager)
{
_profileManager = profileManager;
_templateManager = templateManager;
_armatureManager = armatureManager;
_objectManager = objectManager;
}
public void Draw()
{
var showProfiles = ImGui.CollapsingHeader($"Profiles ({_profileManager.Profiles.Count})###profiles_header");
if (showProfiles)
DrawProfiles();
var showTemplates = ImGui.CollapsingHeader($"Templates ({_templateManager.Templates.Count})###templates_header");
if (showTemplates)
DrawTemplates();
var showArmatures = ImGui.CollapsingHeader($"Armatures ({_armatureManager.Armatures.Count})###armatures_header");
if (showArmatures)
DrawArmatures();
var showObjectManager = ImGui.CollapsingHeader($"Object manager ({_objectManager.Count})###objectmanager_header");
if (showObjectManager)
DrawObjectManager();
}
private void DrawProfiles()
{
foreach (var profile in _profileManager.Profiles.OrderByDescending(x => x.Enabled))
{
DrawSingleProfile("root", profile);
ImGui.Spacing();
ImGui.Spacing();
}
}
private void DrawTemplates()
{
foreach (var template in _templateManager.Templates)
{
DrawSingleTemplate($"root", template);
ImGui.Spacing();
ImGui.Spacing();
}
}
private void DrawArmatures()
{
foreach (var armature in _armatureManager.Armatures)
{
DrawSingleArmature($"root", armature.Value);
ImGui.Spacing();
ImGui.Spacing();
}
}
private void DrawObjectManager()
{
foreach (var kvPair in _objectManager)
{
var show = ImGui.CollapsingHeader($"{kvPair.Key} ({kvPair.Value.Objects.Count} objects)###object-{kvPair.Key}");
if (!show)
continue;
ImGui.Text($"ActorIdentifier");
ImGui.Text($"PlayerName: {kvPair.Key.PlayerName}");
ImGui.Text($"HomeWorld: {kvPair.Key.HomeWorld}");
ImGui.Text($"Retainer: {kvPair.Key.Retainer}");
ImGui.Text($"Kind: {kvPair.Key.Kind}");
ImGui.Text($"Data id: {kvPair.Key.DataId}");
ImGui.Text($"Index: {kvPair.Key.Index.Index}");
ImGui.Text($"Type: {kvPair.Key.Type}");
ImGui.Text($"Special: {kvPair.Key.Special.ToString()}");
ImGui.Text($"ToName: {kvPair.Key.ToName()}");
ImGui.Text($"ToNameWithoutOwnerName: {kvPair.Key.ToNameWithoutOwnerName()}");
ImGui.Spacing();
ImGui.Spacing();
ImGui.Text($"Objects");
ImGui.Text($"Valid: {kvPair.Value.Valid}");
ImGui.Text($"Label: {kvPair.Value.Label}");
ImGui.Text($"Count: {kvPair.Value.Objects.Count}");
foreach (var item in kvPair.Value.Objects)
{
ImGui.Text($"{item}, valid: {item.Valid}");
}
ImGui.Spacing();
ImGui.Spacing();
}
}
private void DrawSingleProfile(string prefix, Profile profile)
{
var show = ImGui.CollapsingHeader($"[{(profile.Enabled ? "E" : "D")}] {profile.Name} on {profile.CharacterName} [{(profile.IsTemporary ? "Temporary" : "Permanent")}]###{prefix}-profile-{profile.UniqueId}");
if (!show)
return;
ImGui.Text($"ID: {profile.UniqueId}");
ImGui.Text($"Enabled: {(profile.Enabled ? "Enabled" : "Disabled")}");
ImGui.Text($"State : {(profile.IsTemporary ? "Temporary" : "Permanent")}");
ImGui.Text($"Lookup: {(profile.LimitLookupToOwnedObjects ? "Limited lookup" : "Global lookup")}");
var showTemplates = ImGui.CollapsingHeader($"Templates###{prefix}-profile-{profile.UniqueId}-templates");
if (showTemplates)
{
foreach (var template in profile.Templates)
{
DrawSingleTemplate($"profile-{profile.UniqueId}", template);
}
}
if (profile.Armatures.Count > 0)
foreach (var armature in profile.Armatures)
DrawSingleArmature($"profile-{profile.UniqueId}", armature);
else
ImGui.Text("No armatures");
}
private void DrawSingleTemplate(string prefix, Template template)
{
var show = ImGui.CollapsingHeader($"{template.Name}###{prefix}-template-{template.UniqueId}");
if (!show)
return;
ImGui.Text($"ID: {template.UniqueId}");
ImGui.Text($"Bones:");
foreach (var kvPair in template.Bones)
{
ImGui.Text($"{kvPair.Key}: p:{kvPair.Value.Translation} | r: {kvPair.Value.Rotation} | s: {kvPair.Value.Scaling}");
}
}
private void DrawSingleArmature(string prefix, Armature armature)
{
var show = ImGui.CollapsingHeader($"{armature} [{(armature.IsBuilt ? "Built" : "Not built")}, {(armature.IsVisible ? "Visible" : "Not visible")}]###{prefix}-armature-{armature.GetHashCode()}");
if (!show)
return;
if (armature.IsBuilt)
{
ImGui.Text($"Total bones: {armature.TotalBoneCount}");
ImGui.Text($"Partial skeletons: {armature.PartialSkeletonCount}");
ImGui.Text($"Root bone: {armature.MainRootBone}");
}
ImGui.Text($"Profile: {armature.Profile.Name} ({armature.Profile.UniqueId})");
ImGui.Text($"Actor: {armature.ActorIdentifier}");
ImGui.Text($"Protection: {(armature.ProtectedUntil >= DateTime.UtcNow ? "Active" : "NOT active")} [{armature.ProtectedUntil} (UTC)]");
//ImGui.Text("Profile:");
//DrawSingleProfile($"armature-{armature.GetHashCode()}", armature.Profile);
ImGui.Text($"Bone template bindings:");
foreach (var kvPair in armature.BoneTemplateBinding)
{
ImGui.Text($"{kvPair.Key} -> {kvPair.Value.Name} ({kvPair.Value.UniqueId})");
}
}
}

View File

@@ -0,0 +1,101 @@
using Dalamud.Interface.Utility;
using Dalamud.Interface;
using ImGuiNET;
using OtterGui;
using System;
using System.Linq;
using OtterGui.Raii;
using System.Numerics;
namespace CustomizePlus.UI.Windows.MainWindow.Tabs;
public static class HeaderDrawer
{
public struct Button
{
public static readonly Button Invisible = new()
{
Visible = false,
Width = 0,
};
public Action? OnClick;
public string Description = string.Empty;
public float Width;
public uint BorderColor;
public uint TextColor;
public FontAwesomeIcon Icon;
public bool Disabled;
public bool Visible;
public Button()
{
Visible = true;
Width = ImGui.GetFrameHeightWithSpacing();
BorderColor = ColorId.HeaderButtons.Value();
TextColor = ColorId.HeaderButtons.Value();
Disabled = false;
}
public readonly void Draw()
{
if (!Visible)
return;
using var color = ImRaii.PushColor(ImGuiCol.Border, BorderColor)
.Push(ImGuiCol.Text, TextColor, TextColor != 0);
if (ImGuiUtil.DrawDisabledButton(Icon.ToIconString(), new Vector2(Width, ImGui.GetFrameHeight()), string.Empty, Disabled, true))
OnClick?.Invoke();
color.Pop();
ImGuiUtil.HoverTooltip(Description);
}
public static Button IncognitoButton(bool current, Action<bool> setter)
=> current
? new Button
{
Description = "Toggle incognito mode off.",
Icon = FontAwesomeIcon.EyeSlash,
OnClick = () => setter(false),
}
: new Button
{
Description = "Toggle incognito mode on.",
Icon = FontAwesomeIcon.Eye,
OnClick = () => setter(true),
};
}
public static void Draw(string text, uint textColor, uint frameColor, int leftButtons, params Button[] buttons)
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0)
.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale);
var leftButtonSize = 0f;
foreach (var button in buttons.Take(leftButtons).Where(b => b.Visible))
{
button.Draw();
ImGui.SameLine();
leftButtonSize += button.Width;
}
var rightButtonSize = buttons.Length > leftButtons ? buttons.Skip(leftButtons).Where(b => b.Visible).Select(b => b.Width).Sum() : 0f;
var midSize = ImGui.GetContentRegionAvail().X - rightButtonSize - ImGuiHelpers.GlobalScale;
style.Pop();
style.Push(ImGuiStyleVar.ButtonTextAlign, new Vector2(0.5f + (rightButtonSize - leftButtonSize) / midSize, 0.5f));
if (textColor != 0)
ImGuiUtil.DrawTextButton(text, new Vector2(midSize, ImGui.GetFrameHeight()), frameColor, textColor);
else
ImGuiUtil.DrawTextButton(text, new Vector2(midSize, ImGui.GetFrameHeight()), frameColor);
style.Pop();
style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale);
foreach (var button in buttons.Skip(leftButtons).Where(b => b.Visible))
{
ImGui.SameLine();
button.Draw();
}
}
}

View File

@@ -0,0 +1,16 @@
using OtterGui.Classes;
namespace CustomizePlus.UI.Windows.MainWindow.Tabs;
public class MessagesTab
{
private readonly MessageService _messages;
public MessagesTab(MessageService messages)
=> _messages = messages;
public bool IsVisible
=> _messages.Count > 0;
public void Draw() => _messages.Draw();
}

View File

@@ -0,0 +1,237 @@
using Dalamud.Interface;
using Dalamud.Plugin.Services;
using ImGuiNET;
using OtterGui.Classes;
using OtterGui.FileSystem.Selector;
using OtterGui.Filesystem;
using OtterGui.Log;
using OtterGui;
using System;
using static CustomizePlus.UI.Windows.MainWindow.Tabs.Profiles.ProfileFileSystemSelector;
using OtterGui.Raii;
using System.Numerics;
using System.Reflection;
using CustomizePlus.Profiles;
using CustomizePlus.Configuration.Data;
using CustomizePlus.Profiles.Data;
using CustomizePlus.Game.Services;
using CustomizePlus.Profiles.Events;
namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Profiles;
public class ProfileFileSystemSelector : FileSystemSelector<Profile, ProfileState>
{
private readonly PluginConfiguration _configuration;
private readonly ProfileManager _profileManager;
private readonly ProfileChanged _event;
private readonly GameObjectService _gameObjectService;
private Profile? _cloneProfile;
private string _newName = string.Empty;
public bool IncognitoMode
{
get => _configuration.UISettings.IncognitoMode;
set
{
_configuration.UISettings.IncognitoMode = value;
_configuration.Save();
}
}
public struct ProfileState
{
public ColorId Color;
}
public ProfileFileSystemSelector(
ProfileFileSystem fileSystem,
IKeyState keyState,
Logger logger,
PluginConfiguration configuration,
ProfileManager profileManager,
ProfileChanged @event,
GameObjectService gameObjectService)
: base(fileSystem, keyState, logger, allowMultipleSelection: true)
{
_configuration = configuration;
_profileManager = profileManager;
_event = @event;
_gameObjectService = gameObjectService;
_event.Subscribe(OnProfileChange, ProfileChanged.Priority.ProfileFileSystemSelector);
AddButton(NewButton, 0);
AddButton(CloneButton, 20);
AddButton(DeleteButton, 1000);
SetFilterTooltip();
}
public void Dispose()
{
base.Dispose();
_event.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<Profile>.Leaf leaf, in ProfileState 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 DrawPopups()
{
DrawNewProfilePopup();
}
private void DrawNewProfilePopup()
{
if (!ImGuiUtil.OpenNameField("##NewProfile", ref _newName))
return;
if (_cloneProfile != null)
{
_profileManager.Clone(_cloneProfile, _newName, true);
_cloneProfile = null;
}
else
{
_profileManager.Create(_newName, true);
}
_newName = string.Empty;
}
private void OnProfileChange(ProfileChanged.Type type, Profile? profile, object? arg3 = null)
{
switch (type)
{
case ProfileChanged.Type.Created:
case ProfileChanged.Type.Deleted:
case ProfileChanged.Type.Renamed:
case ProfileChanged.Type.Toggled:
case ProfileChanged.Type.ChangedCharacterName:
case ProfileChanged.Type.ReloadedAll:
SetFilterDirty();
break;
}
}
private void NewButton(Vector2 size)
{
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), size, "Create a new profile with default configuration.", false,
true))
return;
ImGui.OpenPopup("##NewProfile");
}
private void CloneButton(Vector2 size)
{
var tt = SelectedLeaf == null
? "No profile selected."
: "Clone the currently selected profile to a duplicate";
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clone.ToIconString(), size, tt, SelectedLeaf == null, true))
return;
_cloneProfile = Selected!;
ImGui.OpenPopup("##NewProfile");
}
private void DeleteButton(Vector2 size)
=> DeleteSelectionButton(size, _configuration.UISettings.DeleteTemplateModifier, "profile", "profiles", _profileManager.Delete);
#region Filters
private const StringComparison IgnoreCase = StringComparison.OrdinalIgnoreCase;
private LowerString _filter = LowerString.Empty;
private int _filterType = -1;
private void SetFilterTooltip()
{
FilterTooltip = "Filter profiles for those where their full paths or names contain the given substring.\n"
+ "Enter n:[string] to filter only for profile 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<Profile>.IPath path, out ProfileState state)
{
if (path is ProfileFileSystem.Folder f)
{
state = default;
return FilterValue.Length > 0 && !f.FullName().Contains(FilterValue, IgnoreCase);
}
return ApplyFiltersAndState((ProfileFileSystem.Leaf)path, out state);
}
/// <summary> Apply the string filters. </summary>
private bool ApplyStringFilters(ProfileFileSystem.Leaf leaf, Profile profile)
{
return _filterType switch
{
-1 => false,
0 => !(_filter.IsContained(leaf.FullName()) || profile.Name.Contains(_filter)),
1 => !profile.Name.Contains(_filter),
_ => false, // Should never happen
};
}
/// <summary> Combined wrapper for handling all filters and setting state. </summary>
private bool ApplyFiltersAndState(ProfileFileSystem.Leaf leaf, out ProfileState state)
{
//Do not display temporary profiles;
if (leaf.Value.IsTemporary)
{
state.Color = ColorId.DisabledProfile;
return false;
}
if (leaf.Value.Enabled)
state.Color = leaf.Value.CharacterName == _gameObjectService.GetCurrentPlayerName() ? ColorId.LocalCharacterEnabledProfile : ColorId.EnabledProfile;
else
state.Color = leaf.Value.CharacterName == _gameObjectService.GetCurrentPlayerName() ? ColorId.LocalCharacterDisabledProfile : ColorId.DisabledProfile;
return ApplyStringFilters(leaf, leaf.Value);
}
#endregion
}

View File

@@ -0,0 +1,307 @@
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using System;
using System.Linq;
using System.Numerics;
using CustomizePlus.Profiles;
using CustomizePlus.Game.Services;
using CustomizePlus.Configuration.Data;
using CustomizePlus.Profiles.Data;
using CustomizePlus.UI.Windows.Controls;
namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Profiles;
public class ProfilePanel
{
private readonly ProfileFileSystemSelector _selector;
private readonly ProfileManager _manager;
private readonly PluginConfiguration _configuration;
private readonly TemplateCombo _templateCombo;
private readonly GameStateService _gameStateService;
private string? _newName;
private string? _newCharacterName;
private Profile? _changedProfile;
private Action? _endAction;
private int _dragIndex = -1;
private string SelectionName
=> _selector.Selected == null ? "No Selection" : _selector.IncognitoMode ? _selector.Selected.Incognito : _selector.Selected.Name.Text;
public ProfilePanel(
ProfileFileSystemSelector selector,
ProfileManager manager,
PluginConfiguration configuration,
TemplateCombo templateCombo,
GameStateService gameStateService)
{
_selector = selector;
_manager = manager;
_configuration = configuration;
_templateCombo = templateCombo;
_gameStateService = gameStateService;
}
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 profile editable.",
Icon = FontAwesomeIcon.Lock,
OnClick = () => _manager.SetWriteProtection(_selector.Selected!, false)
}
: new HeaderDrawer.Button
{
Description = "Write-protect this profile.",
Icon = FontAwesomeIcon.LockOpen,
OnClick = () => _manager.SetWriteProtection(_selector.Selected!, true)
};
private void DrawHeader()
=> HeaderDrawer.Draw(SelectionName, 0, ImGui.GetColorU32(ImGuiCol.FrameBg),
0, 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 Profiles");
ImGui.Separator();
using var table = ImRaii.Table("profile", 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 ProfileFileSystem.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 ProfileFileSystem.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;
DrawEnabledSetting();
using (var disabled = ImRaii.Disabled(_selector.Selected?.IsWriteProtected ?? true))
{
DrawBasicSettings();
DrawTemplateArea();
}
}
private void DrawEnabledSetting()
{
var spacing = ImGui.GetStyle().ItemInnerSpacing with { X = ImGui.GetStyle().ItemSpacing.X, Y = ImGui.GetStyle().ItemSpacing.Y };
using (var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing))
{
var enabled = _selector.Selected?.Enabled ?? false;
if (ImGui.Checkbox("##Enabled", ref enabled))
_manager.SetEnabled(_selector.Selected!, enabled);
ImGuiUtil.LabeledHelpMarker("Enabled",
"Whether the templates in this profile should be applied at all. Only one profile can be enabled for a character at the same time.");
ImGui.SameLine();
var isDefault = _manager.DefaultProfile == _selector.Selected;
if (ImGui.Checkbox("##DefaultProfile", ref isDefault))
_manager.SetDefaultProfile(isDefault ? _selector.Selected! : null);
ImGuiUtil.LabeledHelpMarker("Default profile (Players and Retainers only)",
"Whether the templates in this profile are applied to all players and retainers without a specific profile. Only one profile can be default at the same time.");
}
}
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("Profile 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("##ProfileName", ref name, 128))
{
_newName = name;
_changedProfile = _selector.Selected;
}
if (ImGui.IsItemDeactivatedAfterEdit() && _changedProfile != null)
{
_manager.Rename(_changedProfile, name);
_newName = null;
_changedProfile = null;
}
}
else
ImGui.TextUnformatted(_selector.Selected!.Incognito);
ImGui.TableNextRow();
ImGuiUtil.DrawFrameColumn("Character Name");
ImGui.TableNextColumn();
width = new Vector2(ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize("Limit to my creatures").X - 68, 0);
name = _newCharacterName ?? _selector.Selected!.CharacterName;
ImGui.SetNextItemWidth(width.X);
using (var disabled = ImRaii.Disabled(_gameStateService.GameInPosingMode()))
{
if (!_selector.IncognitoMode)
{
if (ImGui.InputText("##CharacterName", ref name, 128))
{
_newCharacterName = name;
_changedProfile = _selector.Selected;
}
if (ImGui.IsItemDeactivatedAfterEdit() && _changedProfile != null)
{
_manager.ChangeCharacterName(_changedProfile, name);
_newCharacterName = null;
_changedProfile = null;
}
}
else
ImGui.TextUnformatted("Incognito active");
ImGui.SameLine();
var enabled = _selector.Selected?.LimitLookupToOwnedObjects ?? false;
if (ImGui.Checkbox("##LimitLookupToOwnedObjects", ref enabled))
_manager.SetLimitLookupToOwned(_selector.Selected!, enabled);
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.");
}
}
}
}
private void DrawTemplateArea()
{
using var disabled = ImRaii.Disabled(_gameStateService.GameInPosingMode());
using var table = ImRaii.Table("SetTable", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX | ImGuiTableFlags.ScrollY);
if (!table)
return;
ImGui.TableSetupColumn("##del", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight());
ImGui.TableSetupColumn("##Index", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Template", ImGuiTableColumnFlags.WidthFixed, 220 * ImGuiHelpers.GlobalScale);
ImGui.TableHeadersRow();
//warn: .ToList() might be performance critical at some point
//the copying via ToList is done because manipulations with .Templates list result in "Collection was modified" exception here
foreach (var (template, idx) in _selector.Selected!.Templates.WithIndex().ToList())
{
using var id = ImRaii.PushId(idx);
ImGui.TableNextColumn();
var keyValid = _configuration.UISettings.DeleteTemplateModifier.IsActive();
var tt = keyValid
? "Remove this template from the profile."
: $"Remove this template from the profile.\nHold {_configuration.UISettings.DeleteTemplateModifier} to remove.";
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), new Vector2(ImGui.GetFrameHeight()), tt, !keyValid, true))
_endAction = () => _manager.DeleteTemplate(_selector.Selected!, idx);
ImGui.TableNextColumn();
ImGui.Selectable($"#{idx + 1:D2}");
DrawDragDrop(_selector.Selected!, idx);
ImGui.TableNextColumn();
_templateCombo.Draw(_selector.Selected!, template, idx);
DrawDragDrop(_selector.Selected!, idx);
}
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("New");
ImGui.TableNextColumn();
_templateCombo.Draw(_selector.Selected!, null, -1);
ImGui.TableNextRow();
_endAction?.Invoke();
_endAction = null;
}
private void DrawDragDrop(Profile profile, int index)
{
const string dragDropLabel = "TemplateDragDrop";
using (var target = ImRaii.DragDropTarget())
{
if (target.Success && ImGuiUtil.IsDropping(dragDropLabel))
{
if (_dragIndex >= 0)
{
var idx = _dragIndex;
_endAction = () => _manager.MoveTemplate(profile, idx, index);
}
_dragIndex = -1;
}
}
using (var source = ImRaii.DragDropSource())
{
if (source)
{
ImGui.TextUnformatted($"Moving template #{index + 1:D2}...");
if (ImGui.SetDragDropPayload(dragDropLabel, nint.Zero, 0))
{
_dragIndex = index;
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,223 @@
//using CustomizePlus.UI.Windows.Debug;
using Dalamud.Interface;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.Utility;
using ImGuiNET;
using OtterGui.Classes;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Widgets;
using System.Diagnostics;
using System.Numerics;
using CustomizePlus.Core.Services;
using CustomizePlus.Configuration.Data;
using CustomizePlus.Profiles;
using CustomizePlus.Templates;
using CustomizePlus.Core.Helpers;
namespace CustomizePlus.UI.Windows.MainWindow.Tabs;
public class SettingsTab
{
private const uint DiscordColor = 0xFFDA8972;
private readonly PluginConfiguration _configuration;
private readonly TemplateManager _templateManager;
private readonly ProfileManager _profileManager;
private readonly HookingService _hookingService;
private readonly SaveService _saveService;
private readonly TemplateEditorManager _templateEditorManager;
private readonly CPlusChangeLog _changeLog;
private readonly MessageService _messageService;
public SettingsTab(
PluginConfiguration configuration,
TemplateManager templateManager,
ProfileManager profileManager,
HookingService hookingService,
SaveService saveService,
TemplateEditorManager templateEditorManager,
CPlusChangeLog changeLog,
MessageService messageService)
{
_configuration = configuration;
_templateManager = templateManager;
_profileManager = profileManager;
_hookingService = hookingService;
_saveService = saveService;
_templateEditorManager = templateEditorManager;
_changeLog = changeLog;
_messageService = messageService;
}
public void Draw()
{
using var child = ImRaii.Child("MainWindowChild");
if (!child)
return;
DrawGeneralSettings();
ImGui.NewLine();
ImGui.NewLine();
using (var child2 = ImRaii.Child("SettingsChild"))
{
DrawInterface();
DrawAdvancedSettings();
}
DrawSupportButtons();
}
#region General Settings
// General Settings
private void DrawGeneralSettings()
{
DrawPluginEnabledCheckbox();
}
private void DrawPluginEnabledCheckbox()
{
using (var disabled = ImRaii.Disabled(_templateEditorManager.IsEditorActive))
{
var isChecked = _configuration.PluginEnabled;
//users doesn't really need to know what exactly this checkbox does so we just tell them it toggles all profiles
if (CtrlHelper.CheckboxWithTextAndHelp("##pluginenabled", "Enable Customize+",
"Globally enables or disables all plugin functionality.", ref isChecked))
{
_configuration.PluginEnabled = isChecked;
_configuration.Save();
_hookingService.ReloadHooks();
}
}
}
#endregion
#region Interface Settings
private void DrawInterface()
{
var isShouldDraw = ImGui.CollapsingHeader("Interface");
if (!isShouldDraw)
return;
DrawHideWindowInCutscene();
DrawFoldersDefaultOpen();
if (Widget.DoubleModifierSelector("Template Deletion Modifier",
"A modifier you need to hold while clicking the Delete Template button for it to take effect.", 100 * ImGuiHelpers.GlobalScale,
_configuration.UISettings.DeleteTemplateModifier, v => _configuration.UISettings.DeleteTemplateModifier = v))
_configuration.Save();
}
private void DrawHideWindowInCutscene()
{
var isChecked = _configuration.UISettings.HideWindowInCutscene;
if (CtrlHelper.CheckboxWithTextAndHelp("##hidewindowincutscene", "Hide plugin windows in cutscenes",
"Controls whether any Fantasia+ windows are hidden during cutscenes or not.", ref isChecked))
{
_configuration.UISettings.HideWindowInCutscene = isChecked;
_configuration.Save();
}
}
private void DrawFoldersDefaultOpen()
{
var isChecked = _configuration.UISettings.FoldersDefaultOpen;
if (CtrlHelper.CheckboxWithTextAndHelp("##foldersdefaultopen", "Open all folders by default",
"Controls whether folders in template and profile lists are open by default or not.", ref isChecked))
{
_configuration.UISettings.FoldersDefaultOpen = isChecked;
_configuration.Save();
}
}
#endregion
#region Advanced Settings
// Advanced Settings
private void DrawAdvancedSettings()
{
var isShouldDraw = ImGui.CollapsingHeader("Advanced");
if (!isShouldDraw)
return;
ImGui.NewLine();
CtrlHelper.LabelWithIcon(FontAwesomeIcon.ExclamationTriangle,
"These are advanced settings. Enable them at your own risk.");
ImGui.NewLine();
DrawEnableRootPositionCheckbox();
DrawDebugModeCheckbox();
}
private void DrawEnableRootPositionCheckbox()
{
var isChecked = _configuration.EditorConfiguration.RootPositionEditingEnabled;
if (CtrlHelper.CheckboxWithTextAndHelp("##rootpos", "Root editing",
"Enables ability to edit the root bones.", ref isChecked))
{
_configuration.EditorConfiguration.RootPositionEditingEnabled = isChecked;
_configuration.Save();
}
}
private void DrawDebugModeCheckbox()
{
var isChecked = _configuration.DebuggingModeEnabled;
if (CtrlHelper.CheckboxWithTextAndHelp("##debugmode", "Debug mode",
"Enables debug mode", ref isChecked))
{
_configuration.DebuggingModeEnabled = isChecked;
_configuration.Save();
}
}
#endregion
#region Support Area
private void DrawSupportButtons()
{
var width = ImGui.CalcTextSize("Join Discord for Support").X + ImGui.GetStyle().FramePadding.X * 2;
var xPos = ImGui.GetWindowWidth() - width;
// Respect the scroll bar width.
if (ImGui.GetScrollMaxY() > 0)
xPos -= ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().FramePadding.X;
ImGui.SetCursorPos(new Vector2(xPos, 0));
DrawDiscordButton(width);
ImGui.SetCursorPos(new Vector2(xPos, 1 * ImGui.GetFrameHeightWithSpacing()));
if (ImGui.Button("Show update history", new Vector2(width, 0)))
_changeLog.Changelog.ForceOpen = true;
}
/// <summary> Draw a button to open the official discord server. </summary>
private void DrawDiscordButton(float width)
{
const string address = @"https://discord.gg/KvGJCCnG8t";
using var color = ImRaii.PushColor(ImGuiCol.Button, DiscordColor);
if (ImGui.Button("Join Discord for Support", new Vector2(width, 0)))
try
{
var process = new ProcessStartInfo(address)
{
UseShellExecute = true,
};
Process.Start(process);
}
catch
{
_messageService.NotificationMessage($"Unable to open Discord at {address}.", NotificationType.Error, false);
}
ImGuiUtil.HoverTooltip($"Open {address}");
}
#endregion
}

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