Code commit
This commit is contained in:
48
CustomizePlus/UI/CPlusWindowSystem.cs
Normal file
48
CustomizePlus/UI/CPlusWindowSystem.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using System;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using CustomizePlus.UI.Windows.MainWindow;
|
||||
using CustomizePlus.UI.Windows;
|
||||
|
||||
namespace CustomizePlus.UI;
|
||||
|
||||
public class CPlusWindowSystem : IDisposable
|
||||
{
|
||||
private readonly WindowSystem _windowSystem = new("Customize+");
|
||||
private readonly UiBuilder _uiBuilder;
|
||||
private readonly MainWindow _mainWindow;
|
||||
private readonly PopupSystem _popupSystem;
|
||||
|
||||
public CPlusWindowSystem(
|
||||
UiBuilder uiBuilder,
|
||||
MainWindow mainWindow,
|
||||
CPlusChangeLog changelog,
|
||||
PopupSystem popupSystem,
|
||||
PluginConfiguration configuration)
|
||||
{
|
||||
_uiBuilder = uiBuilder;
|
||||
_mainWindow = mainWindow;
|
||||
_popupSystem = popupSystem;
|
||||
|
||||
_windowSystem.AddWindow(mainWindow);
|
||||
_windowSystem.AddWindow(changelog.Changelog);
|
||||
_uiBuilder.Draw += OnDraw;
|
||||
_uiBuilder.OpenConfigUi += _mainWindow.Toggle;
|
||||
|
||||
_uiBuilder.DisableGposeUiHide = true;
|
||||
_uiBuilder.DisableCutsceneUiHide = !configuration.UISettings.HideWindowInCutscene;
|
||||
}
|
||||
|
||||
private void OnDraw()
|
||||
{
|
||||
_windowSystem.Draw();
|
||||
_popupSystem.Draw();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_uiBuilder.Draw -= _windowSystem.Draw;
|
||||
_uiBuilder.OpenConfigUi -= _mainWindow.Toggle;
|
||||
}
|
||||
}
|
||||
51
CustomizePlus/UI/Colors.cs
Normal file
51
CustomizePlus/UI/Colors.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CustomizePlus.UI;
|
||||
|
||||
public enum ColorId
|
||||
{
|
||||
UsedTemplate,
|
||||
UnusedTemplate,
|
||||
EnabledProfile,
|
||||
DisabledProfile,
|
||||
LocalCharacterEnabledProfile,
|
||||
LocalCharacterDisabledProfile,
|
||||
FolderExpanded,
|
||||
FolderCollapsed,
|
||||
FolderLine,
|
||||
HeaderButtons,
|
||||
}
|
||||
|
||||
public static class Colors
|
||||
{
|
||||
public const uint SelectedRed = 0xFF2020D0;
|
||||
|
||||
public static (uint DefaultColor, string Name, string Description) Data(this ColorId color)
|
||||
=> color switch
|
||||
{
|
||||
// @formatter:off
|
||||
ColorId.UsedTemplate => (0xFFFFFFFF, "Used Template", "A template which is being used by at least one profile."),
|
||||
//ColorId.EnabledTemplate => (0xFFA0F0A0, "Enabled Automation Set", "An automation set that is currently enabled. Only one set can be enabled for each identifier at once."),
|
||||
ColorId.UnusedTemplate => (0xFF808080, "Unused template", "Template which is currently not being used by any profile."),
|
||||
ColorId.EnabledProfile => (0xFFFFFFFF, "Enabled profile", "A profile which is currently enabled."),
|
||||
ColorId.DisabledProfile => (0xFF808080, "Disabled profile", "A profile which is currently disabled"),
|
||||
ColorId.LocalCharacterEnabledProfile => (0xFF18C018, "Current character profile (enabled)", "A profile which is currently enabled and associated with your character."),
|
||||
ColorId.LocalCharacterDisabledProfile => (0xFF808080, "Current character profile (disabled)", "A profile which is currently disabled and associated with your character."),
|
||||
ColorId.FolderExpanded => (0xFFFFF0C0, "Expanded Folder", "A folder that is currently expanded."),
|
||||
ColorId.FolderCollapsed => (0xFFFFF0C0, "Collapsed Folder", "A folder that is currently collapsed."),
|
||||
ColorId.FolderLine => (0xFFFFF0C0, "Expanded Folder Line", "The line signifying which descendants belong to an expanded folder."),
|
||||
ColorId.HeaderButtons => (0xFFFFF0C0, "Header Buttons", "The text and border color of buttons in the header, like the write protection toggle."),
|
||||
_ => (0x00000000, string.Empty, string.Empty),
|
||||
// @formatter:on
|
||||
};
|
||||
|
||||
private static IReadOnlyDictionary<ColorId, uint> _colors = new Dictionary<ColorId, uint>();
|
||||
|
||||
/// <summary> Obtain the configured value for a color. </summary>
|
||||
public static uint Value(this ColorId color)
|
||||
=> _colors.TryGetValue(color, out var value) ? value : color.Data().DefaultColor;
|
||||
|
||||
/// <summary> Set the configurable colors dictionary to a value. </summary>
|
||||
/*public static void SetColors(Configuration config)
|
||||
=> _colors = config.Colors;*/
|
||||
}
|
||||
60
CustomizePlus/UI/Windows/CPlusChangeLog.cs
Normal file
60
CustomizePlus/UI/Windows/CPlusChangeLog.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using OtterGui.Widgets;
|
||||
|
||||
namespace CustomizePlus.UI.Windows;
|
||||
|
||||
public class CPlusChangeLog
|
||||
{
|
||||
public const int LastChangelogVersion = 0;
|
||||
private readonly PluginConfiguration _config;
|
||||
public readonly Changelog Changelog;
|
||||
|
||||
public CPlusChangeLog(PluginConfiguration config)
|
||||
{
|
||||
_config = config;
|
||||
Changelog = new Changelog("Customize+ update history", ConfigData, Save);
|
||||
|
||||
Add2_0_0_0(Changelog);
|
||||
}
|
||||
|
||||
private (int, ChangeLogDisplayType) ConfigData()
|
||||
=> (_config.ChangelogSettings.LastSeenVersion, _config.ChangelogSettings.ChangeLogDisplayType);
|
||||
|
||||
private void Save(int version, ChangeLogDisplayType type)
|
||||
{
|
||||
_config.ChangelogSettings.LastSeenVersion = version;
|
||||
_config.ChangelogSettings.ChangeLogDisplayType = type;
|
||||
_config.Save();
|
||||
}
|
||||
private static void Add2_0_0_0(Changelog log)
|
||||
=> log.NextVersion("Version 2.0.0.0")
|
||||
.RegisterHighlight("Major rework of the entire plugin.")
|
||||
.RegisterEntry("Migration of your Customize+ settings and profiles should be performed without any issues.", 1)
|
||||
.RegisterImportant("Old version configuration is backed up in case something goes wrong, please report any issues with configuration migration as soon as possible.", 1)
|
||||
|
||||
.RegisterHighlight("Major changes:")
|
||||
|
||||
.RegisterEntry("Plugin has been almost completely rewritten from scratch.", 1)
|
||||
.RegisterImportant("Clipboard copies and profiles from previous versions are not currently supported.", 2)
|
||||
|
||||
.RegisterEntry("User interface has been moved to the framework used by Glamourer and Penumbra, so the interface should feel familiar to the users of those plugins.", 1)
|
||||
.RegisterEntry("User interface issues related to different resolutions and font sizes should *mostly* not occur anymore.", 2)
|
||||
.RegisterImportant("There are several issues with text not fitting in some places depending on your resolution and font size. This will be fixed later.", 3)
|
||||
|
||||
.RegisterEntry("Template system has been added", 1)
|
||||
.RegisterEntry("All bone edits are now stored in templates which can be used by multiple profiles and single profile can reference unlimited number of templates.", 2)
|
||||
|
||||
.RegisterImportant("Chat commands have been changed, refer to \"/customize help\" for information about available commands.", 1)
|
||||
.RegisterEntry("Added \"toggle\" chat command which can be used to toggle given profile on a given character.", 2)
|
||||
|
||||
.RegisterEntry("Profiles can be applied to summons, mounts and pets without any limitations.", 1)
|
||||
.RegisterImportant("Root scaling of mounts is not available for now.", 2)
|
||||
|
||||
.RegisterEntry("Fixed \"Only owned\" not working properly in various cases and renamed it to \"Limit to my creatures\".", 1)
|
||||
|
||||
.RegisterEntry("Fixed profiles \"leaking\" to other characters due to the way original Mare Synchronos integration implementation was handled.", 1)
|
||||
|
||||
.RegisterEntry("Compatibility with cutscenes is improved, but that was not extensively tested.", 1)
|
||||
|
||||
.RegisterEntry("Plugin configuration is now being regularly backed up, the backup is located in %appdata%\\XIVLauncher\\backups\\CustomizePlus folder", 1);
|
||||
}
|
||||
102
CustomizePlus/UI/Windows/Controls/PluginStateBlock.cs
Normal file
102
CustomizePlus/UI/Windows/Controls/PluginStateBlock.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using System.Numerics;
|
||||
using CustomizePlus.Core.Services;
|
||||
using CustomizePlus.Game.Services;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using CustomizePlus.UI.Windows.MainWindow.Tabs.Templates;
|
||||
using CustomizePlus.Core.Helpers;
|
||||
|
||||
namespace CustomizePlus.UI.Windows.Controls;
|
||||
|
||||
public class PluginStateBlock
|
||||
{
|
||||
private readonly BoneEditorPanel _boneEditorPanel;
|
||||
private readonly PluginConfiguration _configuration;
|
||||
private readonly GameStateService _gameStateService;
|
||||
private readonly FantasiaPlusDetectService _fantasiaPlusDetectService;
|
||||
|
||||
private static Vector4 normalColor = new Vector4(1, 1, 1, 1);
|
||||
private static Vector4 warnColor = new Vector4(1, 0.5f, 0, 1);
|
||||
private static Vector4 errorColor = new Vector4(1, 0, 0, 1);
|
||||
|
||||
public PluginStateBlock(
|
||||
BoneEditorPanel boneEditorPanel,
|
||||
PluginConfiguration configuration,
|
||||
GameStateService gameStateService,
|
||||
FantasiaPlusDetectService fantasiaPlusDetectService)
|
||||
{
|
||||
_boneEditorPanel = boneEditorPanel;
|
||||
_configuration = configuration;
|
||||
_gameStateService = gameStateService;
|
||||
_fantasiaPlusDetectService = fantasiaPlusDetectService;
|
||||
}
|
||||
|
||||
public void Draw(float yPos)
|
||||
{
|
||||
var severity = PluginStateSeverity.Normal;
|
||||
string? message = null;
|
||||
|
||||
if (_fantasiaPlusDetectService.IsFantasiaPlusInstalled)
|
||||
{
|
||||
severity = PluginStateSeverity.Error;
|
||||
message = $"Fantasia+ detected. The plugin is disabled until Fantasia+ is disabled and the game is restarted.";
|
||||
}
|
||||
else if (_gameStateService.GameInPosingMode())
|
||||
{
|
||||
severity = PluginStateSeverity.Warning;
|
||||
message = $"GPose active. Most editor features are unavailable while you're in this mode.";
|
||||
}
|
||||
else if (!_configuration.PluginEnabled)
|
||||
{
|
||||
severity = PluginStateSeverity.Warning;
|
||||
message = "Plugin is disabled, template bone editing is not available.";
|
||||
}
|
||||
else if (_boneEditorPanel.IsEditorActive)
|
||||
{
|
||||
if (!_boneEditorPanel.IsCharacterFound)
|
||||
{
|
||||
severity = PluginStateSeverity.Error;
|
||||
message = $"Selected preview character was not found.";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_boneEditorPanel.HasChanges)
|
||||
severity = PluginStateSeverity.Warning;
|
||||
|
||||
message = $"Editor is active.{(_boneEditorPanel.HasChanges ? " You have unsaved changes, finish template bone editing to open save/revert dialog." : "")}";
|
||||
}
|
||||
}
|
||||
|
||||
if (message != null)
|
||||
{
|
||||
ImGui.SetCursorPos(new Vector2(ImGui.GetWindowContentRegionMax().X - ImGui.CalcTextSize(message).X - 30, yPos - ImGuiHelpers.GlobalScale));
|
||||
|
||||
var icon = FontAwesomeIcon.InfoCircle;
|
||||
var color = normalColor;
|
||||
switch (severity)
|
||||
{
|
||||
case PluginStateSeverity.Warning:
|
||||
icon = FontAwesomeIcon.ExclamationTriangle;
|
||||
color = warnColor;
|
||||
break;
|
||||
case PluginStateSeverity.Error:
|
||||
icon = FontAwesomeIcon.ExclamationTriangle;
|
||||
color = errorColor;
|
||||
break;
|
||||
}
|
||||
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, color);
|
||||
CtrlHelper.LabelWithIcon(icon, message, false);
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
}
|
||||
|
||||
private enum PluginStateSeverity
|
||||
{
|
||||
Normal,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
}
|
||||
151
CustomizePlus/UI/Windows/Controls/TemplateCombo.cs
Normal file
151
CustomizePlus/UI/Windows/Controls/TemplateCombo.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using Dalamud.Interface.Utility;
|
||||
using ImGuiNET;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Log;
|
||||
using OtterGui.Widgets;
|
||||
using OtterGui;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using CustomizePlus.Templates;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using CustomizePlus.Profiles;
|
||||
using CustomizePlus.Profiles.Data;
|
||||
using CustomizePlus.Templates.Events;
|
||||
using CustomizePlus.Templates.Data;
|
||||
|
||||
namespace CustomizePlus.UI.Windows.Controls;
|
||||
|
||||
public abstract class TemplateComboBase : FilterComboCache<Tuple<Template, string>>, IDisposable
|
||||
{
|
||||
private readonly PluginConfiguration _configuration;
|
||||
private readonly TemplateChanged _templateChanged;
|
||||
// protected readonly TabSelected TabSelected;
|
||||
protected float InnerWidth;
|
||||
|
||||
protected TemplateComboBase(
|
||||
Func<IReadOnlyList<Tuple<Template, string>>> generator,
|
||||
Logger logger,
|
||||
TemplateChanged templateChanged,
|
||||
//TabSelected tabSelected,
|
||||
PluginConfiguration configuration)
|
||||
: base(generator, logger)
|
||||
{
|
||||
_templateChanged = templateChanged;
|
||||
//TabSelected = tabSelected;
|
||||
_configuration = configuration;
|
||||
_templateChanged.Subscribe(OnTemplateChange, TemplateChanged.Priority.TemplateCombo);
|
||||
}
|
||||
|
||||
public bool Incognito
|
||||
=> _configuration.UISettings.IncognitoMode;
|
||||
|
||||
void IDisposable.Dispose()
|
||||
=> _templateChanged.Unsubscribe(OnTemplateChange);
|
||||
|
||||
protected override bool DrawSelectable(int globalIdx, bool selected)
|
||||
{
|
||||
var ret = base.DrawSelectable(globalIdx, selected);
|
||||
var (design, path) = Items[globalIdx];
|
||||
if (path.Length > 0 && design.Name != path)
|
||||
{
|
||||
var start = ImGui.GetItemRectMin();
|
||||
var pos = start.X + ImGui.CalcTextSize(design.Name).X;
|
||||
var maxSize = ImGui.GetWindowPos().X + ImGui.GetWindowContentRegionMax().X;
|
||||
var remainingSpace = maxSize - pos;
|
||||
var requiredSize = ImGui.CalcTextSize(path).X + ImGui.GetStyle().ItemInnerSpacing.X;
|
||||
var offset = remainingSpace - requiredSize;
|
||||
if (ImGui.GetScrollMaxY() == 0)
|
||||
offset -= ImGui.GetStyle().ItemInnerSpacing.X;
|
||||
|
||||
if (offset < ImGui.GetStyle().ItemSpacing.X)
|
||||
ImGuiUtil.HoverTooltip(path);
|
||||
else
|
||||
ImGui.GetWindowDrawList().AddText(start with { X = pos + offset },
|
||||
ImGui.GetColorU32(ImGuiCol.TextDisabled), path);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected bool Draw(Template? currentTemplate, string? label, float width)
|
||||
{
|
||||
InnerWidth = 400 * ImGuiHelpers.GlobalScale;
|
||||
CurrentSelectionIdx = Math.Max(Items.IndexOf(p => currentTemplate == p.Item1), 0);
|
||||
CurrentSelection = Items[CurrentSelectionIdx];
|
||||
var name = label ?? "Select Template Here...";
|
||||
var ret = Draw("##template", name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing())
|
||||
&& CurrentSelection != null;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected override string ToString(Tuple<Template, string> obj)
|
||||
=> obj.Item1.Name.Text;
|
||||
|
||||
protected override float GetFilterWidth()
|
||||
=> InnerWidth - 2 * ImGui.GetStyle().FramePadding.X;
|
||||
|
||||
protected override bool IsVisible(int globalIndex, LowerString filter)
|
||||
{
|
||||
var (design, path) = Items[globalIndex];
|
||||
return filter.IsContained(path) || design.Name.Lower.Contains(filter.Lower);
|
||||
}
|
||||
|
||||
private void OnTemplateChange(TemplateChanged.Type type, Template template, object? data = null)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case TemplateChanged.Type.Created:
|
||||
case TemplateChanged.Type.Renamed:
|
||||
Cleanup();
|
||||
break;
|
||||
case TemplateChanged.Type.Deleted:
|
||||
Cleanup();
|
||||
if (CurrentSelection?.Item1 == template)
|
||||
{
|
||||
CurrentSelectionIdx = -1;
|
||||
CurrentSelection = null;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TemplateCombo : TemplateComboBase
|
||||
{
|
||||
private readonly ProfileManager _profileManager;
|
||||
|
||||
public TemplateCombo(
|
||||
TemplateManager templateManager,
|
||||
ProfileManager profileManager,
|
||||
TemplateFileSystem fileSystem,
|
||||
Logger logger,
|
||||
TemplateChanged templateChanged,
|
||||
//TabSelected tabSelected,
|
||||
PluginConfiguration configuration)
|
||||
: base(
|
||||
() => templateManager.Templates
|
||||
.Select(d => new Tuple<Template, string>(d, fileSystem.FindLeaf(d, out var l) ? l.FullName() : string.Empty))
|
||||
.OrderBy(d => d.Item2)
|
||||
.ToList(), logger, templateChanged,/* tabSelected, */configuration)
|
||||
{
|
||||
_profileManager = profileManager;
|
||||
}
|
||||
|
||||
public Template? Template
|
||||
=> CurrentSelection?.Item1;
|
||||
|
||||
public void Draw(Profile profile, Template? template, int templateIndex)
|
||||
{
|
||||
if (!Draw(template, Incognito ? template?.Incognito : template?.Name, ImGui.GetContentRegionAvail().X))
|
||||
return;
|
||||
|
||||
if (templateIndex >= 0)
|
||||
_profileManager.ChangeTemplate(profile, templateIndex, CurrentSelection!.Item1);
|
||||
else
|
||||
_profileManager.AddTemplate(profile, CurrentSelection!.Item1);
|
||||
}
|
||||
}
|
||||
145
CustomizePlus/UI/Windows/MainWindow/MainWindow.cs
Normal file
145
CustomizePlus/UI/Windows/MainWindow/MainWindow.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui.Raii;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using SettingsTab = CustomizePlus.UI.Windows.MainWindow.Tabs.SettingsTab;
|
||||
using CustomizePlus.Core.Services;
|
||||
using CustomizePlus.UI.Windows.MainWindow.Tabs.Debug;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using CustomizePlus.UI.Windows.MainWindow.Tabs.Templates;
|
||||
using CustomizePlus.UI.Windows.Controls;
|
||||
using CustomizePlus.UI.Windows.MainWindow.Tabs.Profiles;
|
||||
using CustomizePlus.UI.Windows.MainWindow.Tabs;
|
||||
using CustomizePlus.Templates;
|
||||
|
||||
namespace CustomizePlus.UI.Windows.MainWindow;
|
||||
|
||||
public class MainWindow : Window, IDisposable
|
||||
{
|
||||
private readonly SettingsTab _settingsTab;
|
||||
private readonly TemplatesTab _templatesTab;
|
||||
private readonly ProfilesTab _profilesTab;
|
||||
private readonly MessagesTab _messagesTab;
|
||||
private readonly IPCTestTab _ipcTestTab;
|
||||
private readonly StateMonitoringTab _stateMonitoringTab;
|
||||
|
||||
private readonly PluginStateBlock _pluginStateBlock;
|
||||
|
||||
private readonly TemplateEditorManager _templateEditorManager;
|
||||
private readonly FantasiaPlusDetectService _cPlusDetectService;
|
||||
private readonly PluginConfiguration _configuration;
|
||||
|
||||
public MainWindow(
|
||||
DalamudPluginInterface pluginInterface,
|
||||
SettingsTab settingsTab,
|
||||
TemplatesTab templatesTab,
|
||||
ProfilesTab profilesTab,
|
||||
MessagesTab messagesTab,
|
||||
IPCTestTab ipcTestTab,
|
||||
StateMonitoringTab stateMonitoringTab,
|
||||
PluginStateBlock pluginStateBlock,
|
||||
TemplateEditorManager templateEditorManager,
|
||||
PluginConfiguration configuration,
|
||||
FantasiaPlusDetectService cPlusDetectService
|
||||
) : base($"Customize+ v{Plugin.Version}###CPlusMainWindow")
|
||||
{
|
||||
_settingsTab = settingsTab;
|
||||
_templatesTab = templatesTab;
|
||||
_profilesTab = profilesTab;
|
||||
_messagesTab = messagesTab;
|
||||
_ipcTestTab = ipcTestTab;
|
||||
_stateMonitoringTab = stateMonitoringTab;
|
||||
|
||||
_pluginStateBlock = pluginStateBlock;
|
||||
|
||||
_templateEditorManager = templateEditorManager;
|
||||
_cPlusDetectService = cPlusDetectService;
|
||||
_configuration = configuration;
|
||||
|
||||
pluginInterface.UiBuilder.DisableGposeUiHide = true;
|
||||
SizeConstraints = new WindowSizeConstraints()
|
||||
{
|
||||
MinimumSize = new Vector2(700, 675),
|
||||
MaximumSize = ImGui.GetIO().DisplaySize,
|
||||
};
|
||||
|
||||
IsOpen = pluginInterface.IsDevMenuOpen && configuration.DebuggingModeEnabled;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void Draw()
|
||||
{
|
||||
var yPos = ImGui.GetCursorPosY();
|
||||
|
||||
using (var disabled = ImRaii.Disabled(_cPlusDetectService.IsFantasiaPlusInstalled))
|
||||
{
|
||||
LockWindowClosureIfNeeded();
|
||||
if (ImGui.BeginTabBar("##tabs", ImGuiTabBarFlags.None)) //todo: remember last selected tab
|
||||
{
|
||||
if (ImGui.BeginTabItem("Settings"))
|
||||
{
|
||||
_settingsTab.Draw();
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
|
||||
if (ImGui.BeginTabItem("Templates"))
|
||||
{
|
||||
_templatesTab.Draw();
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
|
||||
if (ImGui.BeginTabItem("Profiles"))
|
||||
{
|
||||
_profilesTab.Draw();
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
|
||||
//if(_messagesTab.IsVisible)
|
||||
//{
|
||||
/*if (ImGui.BeginTabItem("Messages"))
|
||||
{
|
||||
_messagesTab.Draw();
|
||||
ImGui.EndTabItem();
|
||||
}*/
|
||||
//}
|
||||
|
||||
if (_configuration.DebuggingModeEnabled)
|
||||
{
|
||||
if (ImGui.BeginTabItem("IPC Test"))
|
||||
{
|
||||
_ipcTestTab.Draw();
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
|
||||
if (ImGui.BeginTabItem("State monitoring"))
|
||||
{
|
||||
_stateMonitoringTab.Draw();
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_pluginStateBlock.Draw(yPos);
|
||||
}
|
||||
|
||||
private void LockWindowClosureIfNeeded()
|
||||
{
|
||||
if (_templateEditorManager.IsEditorActive)
|
||||
{
|
||||
ShowCloseButton = false;
|
||||
RespectCloseHotkey = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowCloseButton = true;
|
||||
RespectCloseHotkey = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
152
CustomizePlus/UI/Windows/MainWindow/Tabs/Debug/IPCTestTab.cs
Normal file
152
CustomizePlus/UI/Windows/MainWindow/Tabs/Debug/IPCTestTab.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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})");
|
||||
}
|
||||
}
|
||||
}
|
||||
101
CustomizePlus/UI/Windows/MainWindow/Tabs/HeaderDrawer.cs
Normal file
101
CustomizePlus/UI/Windows/MainWindow/Tabs/HeaderDrawer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
16
CustomizePlus/UI/Windows/MainWindow/Tabs/MessagesTab.cs
Normal file
16
CustomizePlus/UI/Windows/MainWindow/Tabs/MessagesTab.cs
Normal 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();
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
223
CustomizePlus/UI/Windows/MainWindow/Tabs/SettingsTab.cs
Normal file
223
CustomizePlus/UI/Windows/MainWindow/Tabs/SettingsTab.cs
Normal 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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
127
CustomizePlus/UI/Windows/PopupSystem.cs
Normal file
127
CustomizePlus/UI/Windows/PopupSystem.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using Dalamud.Interface.Utility;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Log;
|
||||
using OtterGui.Raii;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
|
||||
namespace CustomizePlus.UI.Windows;
|
||||
|
||||
public class PopupSystem
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
private readonly PluginConfiguration _configuration;
|
||||
|
||||
private readonly Dictionary<string, PopupData> _popups = new();
|
||||
private readonly List<PopupData> _displayedPopups = new();
|
||||
|
||||
public PopupSystem(Logger logger, PluginConfiguration configuration)
|
||||
{
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public void RegisterPopup(string name, string text, bool displayOnce = false, Vector2? sizeDividers = null)
|
||||
{
|
||||
name = name.ToLowerInvariant();
|
||||
|
||||
if (_popups.ContainsKey(name))
|
||||
throw new Exception($"Popup \"{name}\" is already registered");
|
||||
|
||||
_popups[name] = new PopupData { Name = name, Text = text, DisplayOnce = displayOnce, SizeDividers = sizeDividers };
|
||||
|
||||
_logger.Debug($"Popup \"{name}\" registered");
|
||||
}
|
||||
|
||||
public void ShowPopup(string name)
|
||||
{
|
||||
name = name.ToLowerInvariant();
|
||||
|
||||
if (!_popups.ContainsKey(name))
|
||||
throw new Exception($"Popup \"{name}\" is not registered");
|
||||
|
||||
var popup = _popups[name];
|
||||
|
||||
if (popup.DisplayRequested || _configuration.UISettings.ViewedMessageWindows.Contains(name))
|
||||
return;
|
||||
|
||||
popup.DisplayRequested = true;
|
||||
|
||||
//_logger.Debug($"Popup \"{name}\" set as requested to be displayed");
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
var viewportSize = ImGui.GetWindowViewport().Size;
|
||||
|
||||
foreach (var popup in _popups.Values)
|
||||
{
|
||||
if (popup.DisplayRequested)
|
||||
_displayedPopups.Add(popup);
|
||||
}
|
||||
|
||||
if (_displayedPopups.Count == 0)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < _displayedPopups.Count; ++i)
|
||||
{
|
||||
var popup = _displayedPopups[i];
|
||||
if (popup.DisplayRequested)
|
||||
{
|
||||
ImGui.OpenPopup(popup.Name);
|
||||
popup.DisplayRequested = false;
|
||||
}
|
||||
|
||||
var xDiv = popup.SizeDividers?.X ?? 5;
|
||||
var yDiv = popup.SizeDividers?.Y ?? 12;
|
||||
|
||||
ImGui.SetNextWindowSize(new Vector2(viewportSize.X / xDiv, viewportSize.Y / yDiv));
|
||||
ImGui.SetNextWindowPos(viewportSize / 2, ImGuiCond.Always, new Vector2(0.5f));
|
||||
using var popupWindow = ImRaii.Popup(popup.Name, ImGuiWindowFlags.Modal);
|
||||
if (!popupWindow)
|
||||
{
|
||||
//fixes bug with windows being closed after going into gpose
|
||||
ImGui.OpenPopup(popup.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
ImGui.SetCursorPos(new Vector2(10, ImGui.GetWindowHeight() / 4));
|
||||
ImGuiUtil.TextWrapped(popup.Text);
|
||||
|
||||
var buttonWidth = new Vector2(150 * ImGuiHelpers.GlobalScale, 0);
|
||||
var yPos = ImGui.GetWindowHeight() - 2 * ImGui.GetFrameHeight();
|
||||
var xPos = (ImGui.GetWindowWidth() - ImGui.GetStyle().ItemSpacing.X - buttonWidth.X) / 2;
|
||||
ImGui.SetCursorPos(new Vector2(xPos, yPos));
|
||||
if (ImGui.Button("Ok", buttonWidth))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
_displayedPopups.RemoveAt(i--);
|
||||
|
||||
if (popup.DisplayOnce)
|
||||
{
|
||||
_configuration.UISettings.ViewedMessageWindows.Add(popup.Name);
|
||||
_configuration.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PopupData
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public string Text { get; set; }
|
||||
|
||||
public bool DisplayRequested { get; set; }
|
||||
|
||||
public bool DisplayOnce { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Divider values used to divide viewport size when setting window size
|
||||
/// </summary>
|
||||
public Vector2? SizeDividers { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user