diff --git a/Ktisis/Configuration.cs b/Ktisis/Configuration.cs index de6da14aa..74771a83d 100644 --- a/Ktisis/Configuration.cs +++ b/Ktisis/Configuration.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Numerics; using System.Collections.Generic; @@ -11,6 +11,7 @@ using Dalamud.Game.ClientState.Keys; using Ktisis.Interface; +using Ktisis.Interface.Modular; using Ktisis.Localization; using Ktisis.Structs.Bones; using Ktisis.Structs.Actor.Equip.SetSources; @@ -34,6 +35,8 @@ public class Configuration : IPluginConfiguration { public bool AutoOpenCtor { get; set; } = false; public OpenKtisisMethod OpenKtisisMethod { get; set; } = OpenKtisisMethod.OnEnterGpose; + public List ModularConfig { get; set; } = new(); + public bool ModularHideDefaultWorkspace { get; set; } = false; public bool DisplayCharName { get; set; } = true; public bool CensorNsfw { get; set; } = true; @@ -138,9 +141,10 @@ public bool IsBoneCategoryVisible(Category category) { public Dictionary SavedDirPaths { get; set; } = new(); - // Data memory + // Data public Dictionary? GlamourPlateData { get; set; } = null; public Dictionary> CustomBoneOffset { get; set; } = new(); + public bool ClipboardExportClearJson { get; set; } = false; // Validate for changes in config versions. diff --git a/Ktisis/Interface/Components/ActorsList.cs b/Ktisis/Interface/Components/ActorsList.cs index 779c90a51..b2a859f3f 100644 --- a/Ktisis/Interface/Components/ActorsList.cs +++ b/Ktisis/Interface/Components/ActorsList.cs @@ -16,7 +16,7 @@ namespace Ktisis.Interface.Components { internal static class ActorsList { private static List SavedObjects = new(); - private static List? SelectorList = null; + internal static List? SelectorList = null; private static string Search = ""; private static readonly HashSet WhitelistObjectKinds = new(){ ObjectKind.Player, @@ -32,7 +32,7 @@ internal static class ActorsList { // Draw - public unsafe static void Draw() { + public unsafe static void Draw(bool isHorizontal = false) { // Prevent displaying the same target multiple time SavedObjects = SavedObjects.Distinct().ToList(); @@ -44,20 +44,26 @@ public unsafe static void Draw() { SavedObjects.RemoveAll(o => !IsValidActor(o)); var buttonSize = new Vector2(ImGui.GetContentRegionAvail().X, ControlButtons.ButtonSize.Y); - if (ImGui.CollapsingHeader("Actor List")) { - long? toRemove = null; - foreach (var pointer in SavedObjects) { - if (!IsValidActor(pointer)) continue; - if (ImGui.Button($"{((Actor*)pointer)->GetNameOrId()}{ExtraInfo(pointer)}##ActorList##{pointer}", buttonSize)) - Services.Targets->GPoseTarget = (GameObject*)pointer; - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - toRemove = pointer; - } - if (toRemove != null) SavedObjects.Remove((long)toRemove); - if (GuiHelpers.IconButtonTooltip(Dalamud.Interface.FontAwesomeIcon.Plus, "Add Actor", ControlButtons.ButtonSize)) - OpenSelector(); + long? toRemove = null; + foreach (var pointer in SavedObjects) { + if (!IsValidActor(pointer)) continue; + + var buttonText = $"{((Actor*)pointer)->GetNameOrId()}{ExtraInfo(pointer)}"; + + if (isHorizontal) buttonSize.X = ImGui.CalcTextSize(buttonText).X + ControlButtons.ButtonSize.X; + if (ImGui.Button($"{buttonText}##ActorList##{pointer}", buttonSize)) + Services.Targets->GPoseTarget = (GameObject*)pointer; + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + toRemove = pointer; + if (isHorizontal) ImGui.SameLine(); + } + if (toRemove != null) SavedObjects.Remove((long)toRemove); + + if (GuiHelpers.IconButtonTooltip(Dalamud.Interface.FontAwesomeIcon.Plus, "Add Actor", ControlButtons.ButtonSize)) + OpenSelector(); + if (!isHorizontal) { ImGui.SameLine(ImGui.GetContentRegionAvail().X - (ImGui.GetStyle().ItemSpacing.X) - GuiHelpers.CalcIconSize(FontAwesomeIcon.InfoCircle).X); ControlButtons.VerticalAlignTextOnButtonSize(); @@ -68,8 +74,6 @@ public unsafe static void Draw() { ImGui.Text("Right click to remove an Actor from the list"); ImGui.EndTooltip(); } - if (SelectorList != null) - DrawListAddActor(); } } @@ -94,7 +98,7 @@ private static void OpenSelector() => private static void CloseSelector() => SelectorList = null; - private unsafe static void DrawListAddActor() { + internal unsafe static void DrawListAddActor() { PopupSelect.HoverPopupWindow( PopupSelect.HoverPopupWindowFlags.SelectorList | PopupSelect.HoverPopupWindowFlags.SearchBar, SelectorList!, diff --git a/Ktisis/Interface/Components/AnimationControls.cs b/Ktisis/Interface/Components/AnimationControls.cs index d08be054a..7586123de 100644 --- a/Ktisis/Interface/Components/AnimationControls.cs +++ b/Ktisis/Interface/Components/AnimationControls.cs @@ -10,18 +10,17 @@ namespace Ktisis.Interface.Components { public static class AnimationControls { - public static unsafe void Draw(GameObject? target) { - // Animation control - if (ImGui.CollapsingHeader("Animation Control")) { - var control = PoseHooks.GetAnimationControl(target); - if (PoseHooks.PosingEnabled || !Ktisis.IsInGPose || PoseHooks.IsGamePlaybackRunning(target) || control == null) { - ImGui.Text("Animation Control is available when:"); - ImGui.BulletText("Game animation is paused"); - ImGui.BulletText("Posing is off"); - } else - AnimationSeekAndSpeed(control); - } + public static unsafe void Draw() { + var target = Ktisis.GPoseTarget; + // Animation control + var control = PoseHooks.GetAnimationControl(target); + if (PoseHooks.PosingEnabled || !Ktisis.IsInGPose || PoseHooks.IsGamePlaybackRunning(target) || control == null) { + ImGui.Text("Animation Control is available when:"); + ImGui.BulletText("Game animation is paused"); + ImGui.BulletText("Posing is off"); + } else + AnimationSeekAndSpeed(control); } public static unsafe void AnimationSeekAndSpeed(hkaDefaultAnimationControl* control) { var duration = control->hkaAnimationControl.Binding.ptr->Animation.ptr->Duration; diff --git a/Ktisis/Interface/Components/BoneTree.cs b/Ktisis/Interface/Components/BoneTree.cs index c59809f27..71a30cb7e 100644 --- a/Ktisis/Interface/Components/BoneTree.cs +++ b/Ktisis/Interface/Components/BoneTree.cs @@ -13,23 +13,24 @@ public class BoneTree { private static Vector2 _FrameMin; private static Vector2 _FrameMax; - public static unsafe void Draw(Actor* actor) { - if (ImGui.CollapsingHeader("Bone List")) { - var lineHeight = ImGui.GetTextLineHeight(); - if (ImGui.BeginChildFrame(471, new Vector2(-1, lineHeight * 12), ImGuiWindowFlags.HorizontalScrollbar)) { - if (actor == null || actor->Model == null || actor->Model->Skeleton == null) - return; - - var body = actor->Model->Skeleton->GetBone(0, 1); - if (body != null && body.Pose != null) - DrawBoneTreeNode(body); - - ImGui.EndChildFrame(); - - _FrameMin = ImGui.GetItemRectMin(); - _FrameMax.X = ImGui.GetItemRectMax().X; - _FrameMax.Y = _FrameMin.Y + lineHeight * 11; - } + public static unsafe void Draw() { + var actor = Ktisis.Target; + if (actor->Model == null) return; + + var lineHeight = ImGui.GetTextLineHeight(); + if (ImGui.BeginChildFrame(471, new Vector2(-1, lineHeight * 12), ImGuiWindowFlags.HorizontalScrollbar)) { + if (actor == null || actor->Model == null || actor->Model->Skeleton == null) + return; + + var body = actor->Model->Skeleton->GetBone(0, 1); + if (body != null && body.Pose != null) + DrawBoneTreeNode(body); + + ImGui.EndChildFrame(); + + _FrameMin = ImGui.GetItemRectMin(); + _FrameMax.X = ImGui.GetItemRectMax().X; + _FrameMax.Y = _FrameMin.Y + lineHeight * 11; } } private unsafe static void DrawBoneTreeNode(Bone bone) { diff --git a/Ktisis/Interface/Components/Categories.cs b/Ktisis/Interface/Components/Categories.cs index faf088773..f82795993 100644 --- a/Ktisis/Interface/Components/Categories.cs +++ b/Ktisis/Interface/Components/Categories.cs @@ -47,6 +47,17 @@ public static bool DrawConfigList(Configuration cfg) { }); } + + public static void DrawToggleListWithHint() { + if (!DrawToggleList(Ktisis.Configuration)) { + ImGui.Text("No bone found."); + ImGui.Text("Show Skeleton ("); + ImGui.SameLine(); + GuiHelpers.Icon(FontAwesomeIcon.EyeSlash); + ImGui.SameLine(); + ImGui.Text(") to fill this."); + } + } public static bool DrawToggleList(Configuration cfg) { if (!DrawList(category => { diff --git a/Ktisis/Interface/Components/ControlButtons.cs b/Ktisis/Interface/Components/ControlButtons.cs index 3ba440fdc..fda284b6c 100644 --- a/Ktisis/Interface/Components/ControlButtons.cs +++ b/Ktisis/Interface/Components/ControlButtons.cs @@ -21,9 +21,19 @@ public static class ControlButtons { private static bool IsSettingsHovered = false; private static bool IsSettingsActive = false; + // utils public static void VerticalAlignTextOnButtonSize(float percentage = 0.667f) => ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (ButtonSize.Y / 2 - ImGui.GetFontSize() * percentage)); // align text with button size + public static void DrawGizmoOperations() { + ButtonChangeOperation(OPERATION.TRANSLATE, IconsPool.Position); + ImGui.SameLine(); + ButtonChangeOperation(OPERATION.ROTATE, IconsPool.Rotation); + ImGui.SameLine(); + ButtonChangeOperation(OPERATION.SCALE, IconsPool.Scale); + ImGui.SameLine(); + ButtonChangeOperation(OPERATION.UNIVERSAL, IconsPool.Universal); + } public static void DrawExtra() { var gizmode = Ktisis.Configuration.GizmoMode; if (GuiHelpers.IconButtonTooltip(gizmode == MODE.WORLD ? FontAwesomeIcon.Globe : FontAwesomeIcon.Home, "Local / World orientation mode switch.", ButtonSize)) @@ -69,17 +79,25 @@ private static void DrawInfo() { GuiHelpers.Tooltip("Information"); } - private static void DrawSettings() { - ImGui.PushStyleColor(ImGuiCol.Button, 0x00000000); - ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 200f); - ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(ImGui.GetFontSize() * 0.25f)); - - if (GuiHelpers.IconButton(FontAwesomeIcon.Cog, new(ImGui.GetFontSize() * 1.5f))) + internal static void DrawSettings(int flags = 1) { + var isTitleDecoration = (flags & 1) != 0; + + Vector2 buttonSize = ButtonSize; + if (isTitleDecoration) { + ImGui.PushStyleColor(ImGuiCol.Button, 0x00000000); + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 200f); + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(ImGui.GetFontSize() * 0.25f)); + buttonSize = new(ImGui.GetFontSize() * 1.5f); + } + + if (GuiHelpers.IconButton(FontAwesomeIcon.Cog, buttonSize)) if (ConfigGui.Visible) ConfigGui.Hide(); else ConfigGui.Show(); - ImGui.PopStyleColor(); - ImGui.PopStyleVar(2); + if (isTitleDecoration) { + ImGui.PopStyleColor(); + ImGui.PopStyleVar(2); + } IsSettingsHovered = ImGui.IsItemHovered(); IsSettingsActive = ImGui.IsItemActive(); @@ -144,6 +162,7 @@ public static void ButtonChangeOperation(OPERATION operation, FontAwesomeIcon ic // Independant from the others public static void DrawPoseSwitch() { + ImGui.AlignTextToFramePadding(); ImGui.SetCursorPosX(ImGui.CalcTextSize("GPose Disabled").X + (ImGui.GetFontSize() * 8) + ImGui.GetStyle().ItemSpacing.X + GuiHelpers.CalcIconSize(FontAwesomeIcon.Cog).X); // Prevents text overlap ImGui.BeginDisabled(!Ktisis.IsInGPose); @@ -156,6 +175,7 @@ public static void DrawPoseSwitch() { if (Ktisis.IsInGPose) ImGui.PopStyleColor(); ImGui.SameLine(); + if (!Ktisis.IsInGPose) ImGuiComponents.DisabledToggleButton("Toggle Posing", false); else @@ -188,5 +208,11 @@ private static string SiblingLinkToTooltip(SiblingLink siblingLink) SiblingLink.RotationMirrorX => "Mirror rotation", _ => "No sibling link" }; + + public static void DrawParentingCheckbox() { + var parent = Ktisis.Configuration.EnableParenting; + if (ImGui.Checkbox("Parenting", ref parent)) + Ktisis.Configuration.EnableParenting = parent; + } } } \ No newline at end of file diff --git a/Ktisis/Interface/Components/PopupSelect.cs b/Ktisis/Interface/Components/PopupSelect.cs index 27629fc47..4902f2f62 100644 --- a/Ktisis/Interface/Components/PopupSelect.cs +++ b/Ktisis/Interface/Components/PopupSelect.cs @@ -107,8 +107,9 @@ public static void HoverPopupWindow( if (ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows) && !ImGui.IsAnyItemActive() && !ImGui.IsMouseClicked(ImGuiMouseButton.Left)) ImGui.SetKeyboardFocusHere(flags.HasFlag(HoverPopupWindowFlags.SearchBar) ? -1 : 0); // TODO: verify the keyboarf focus behaviour when searchbar is disabled + bool isListBoxOpened = false; if (flags.HasFlag(HoverPopupWindowFlags.SelectorList)) - ImGui.BeginListBox(listLabel, new Vector2(-1, 300)); + isListBoxOpened = ImGui.BeginListBox(listLabel, new Vector2(-1, 300)); // box has began if (flags.HasFlag(HoverPopupWindowFlags.Header)) { @@ -172,7 +173,7 @@ public static void HoverPopupWindow( // box has ended - if (flags.HasFlag(HoverPopupWindowFlags.SelectorList)) + if (flags.HasFlag(HoverPopupWindowFlags.SelectorList) && isListBoxOpened) ImGui.EndListBox(); HoverPopupWindowFocus |= ImGui.IsItemActive(); diff --git a/Ktisis/Interface/Components/TransformTable.cs b/Ktisis/Interface/Components/TransformTable.cs index 35476138f..02ddaea12 100644 --- a/Ktisis/Interface/Components/TransformTable.cs +++ b/Ktisis/Interface/Components/TransformTable.cs @@ -68,9 +68,6 @@ public void Update(Vector3 pos, Quaternion rot, Vector3 scale) { public bool DrawTable() { var result = false; - var iconPosition = FontAwesomeIcon.LocationArrow; - var iconRotation = FontAwesomeIcon.Sync; - var iconScale = FontAwesomeIcon.ExpandAlt; FetchConfigurations(); @@ -86,17 +83,17 @@ public bool DrawTable() { // Position result |= ColoredDragFloat3("##Position", ref Position, BaseSpeedPos * multiplier, axisColors); ImGui.SameLine(); - ControlButtons.ButtonChangeOperation(OPERATION.TRANSLATE, iconPosition); - + ControlButtons.ButtonChangeOperation(OPERATION.TRANSLATE, IconsPool.Position); + // Rotation result |= ColoredDragFloat3("##Rotation", ref Rotation, BaseSpeedRot * multiplier, axisColors); ImGui.SameLine(); - ControlButtons.ButtonChangeOperation(OPERATION.ROTATE, iconRotation); - + ControlButtons.ButtonChangeOperation(OPERATION.ROTATE, IconsPool.Rotation); + // Scale result |= ColoredDragFloat3("##Scale", ref Scale, BaseSpeedSca * multiplier, axisColors); ImGui.SameLine(); - ControlButtons.ButtonChangeOperation(OPERATION.SCALE, iconScale); + ControlButtons.ButtonChangeOperation(OPERATION.SCALE, IconsPool.Scale); IsEditing = result; diff --git a/Ktisis/Interface/Modular/Configurator.cs b/Ktisis/Interface/Modular/Configurator.cs new file mode 100644 index 000000000..6861408bb --- /dev/null +++ b/Ktisis/Interface/Modular/Configurator.cs @@ -0,0 +1,297 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Interface; +using ImGuiNET; + +using Ktisis.Interface.Components; +using Ktisis.Util; + +namespace Ktisis.Interface.Modular { + internal class Configurator { + + // Below is config rendering logic + private enum AddItemListMode { Closed, Last, AboveSelected }; + private static AddItemListMode IsAddPanelOpen = AddItemListMode.Closed; + private static string AddPanelSearch = ""; + internal static IModularItem? MovingItem = null; + private static IModularItem? SelectedItem = null; + + public static void DrawConfigTab(Configuration cfg) { + if (ImGui.BeginTabItem("Modular")) { + + if (GuiHelpers.IconButton(FontAwesomeIcon.Plus)) + IsAddPanelOpen = AddItemListMode.Last; + ImGui.SameLine(); + if (GuiHelpers.IconButton(FontAwesomeIcon.Clipboard, default, $"Export##Modular")) + JsonHelpers.ExportClipboard(Ktisis.Configuration.ModularConfig); + ImGui.SameLine(); + if (GuiHelpers.IconButtonHoldConfirm(FontAwesomeIcon.Paste, $"Hold Ctrl and Shift to paste and replace the entire modular configuration.", default, $"Import##Modular")) { + var importModular = JsonHelpers.ImportClipboard>(); + if (importModular != null) + Ktisis.Configuration.ModularConfig = importModular; + } + + ImGui.SameLine(); + var hideDefaultWindow = Ktisis.Configuration.ModularHideDefaultWorkspace; + if (ImGui.Checkbox("Hide Default Window", ref hideDefaultWindow)) + Ktisis.Configuration.ModularHideDefaultWorkspace = hideDefaultWindow; + + ImGui.Columns(2); + if (ImGui.BeginChildFrame(958, new(ImGui.GetContentRegionAvail().X, ImGui.GetIO().DisplaySize.Y * 0.6f))) { + foreach (var item in cfg.ModularConfig) { + if (TreeNode(item)) + break; + } + ImGui.EndChildFrame(); + } + + ImGui.NextColumn(); + DrawItemDetails(SelectedItem); + ImGui.Columns(); + + ImGui.EndTabItem(); + } + + if (IsAddPanelOpen != AddItemListMode.Closed) + DrawAddPanel(); + } + + private static void DrawAddPanel() { + PopupSelect.HoverPopupWindow( + PopupSelect.HoverPopupWindowFlags.SelectorList | PopupSelect.HoverPopupWindowFlags.SearchBar, + Manager.Available, + (e, input) => e.Where(t => t.Name.Contains(input, StringComparison.OrdinalIgnoreCase)), + (t, a) => { // draw Line + bool selected = ImGui.Selectable($"{t.Name}##Modular##AddPanel##{t}", a); + bool focus = ImGui.IsItemFocused(); + ImGui.SameLine(); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.5f); + GuiHelpers.TextRight(TypeToKind(t)); + ImGui.PopStyleVar(); + return (selected, focus); + }, + (t) => { + if (SelectedItem != null && IsAddPanelOpen == AddItemListMode.AboveSelected) + AddBefore(t.Name, SelectedItem); + else if (IsAddPanelOpen == AddItemListMode.Last) + Add(t.Name); + }, + () => IsAddPanelOpen = AddItemListMode.Closed, // on close + ref AddPanelSearch, + "Add Panel", + "##addpanel_select", + "##addpanel_search"); + } + private static string TypeToKind(Type? type) => type!.Namespace!.Split('.').Last(); + private static void Add(string typeName) { + var item = Manager.CreateItemFromTypeName(typeName); + if (item == null) return; + Add(item); + } + + + private static void Add(IModularItem toAdd) { + if (Ktisis.Configuration.ModularConfig == null) + Ktisis.Configuration.ModularConfig = new(); + + if (toAdd is IModularContainer) + Ktisis.Configuration.ModularConfig.Add(toAdd); + else if (Ktisis.Configuration.ModularConfig.Any()) { + var container = (IModularContainer)Ktisis.Configuration.ModularConfig.Last(); + container.Items.Add(toAdd); + } + Manager.Init(); + } + private static void AddBefore(string handle, IModularItem itemBefore) { + var item = Manager.CreateItemFromTypeName(handle); + if (item == null) return; + InsertBefore(Ktisis.Configuration.ModularConfig, item, itemBefore); + } + + private static bool Delete(IModularItem toRemove) { + var pair = DeleteSub(Ktisis.Configuration.ModularConfig, toRemove); + if (pair != null) { + pair.Value.Item1.RemoveAt(pair.Value.Item2); + Manager.Init(); + return true; + } + return false; + } + private static (List,int)? DeleteSub(List items, IModularItem toRemove) { + var index = items.IndexOf(toRemove); + if (index != -1) return (items, index); + + foreach (var cc in items) { + if (cc is IModularContainer container && container.Items.Any()) { + var pair = DeleteSub(container.Items, toRemove); + if (pair != null) + return pair; + } + } + return null; + } + private static bool MoveAt(IModularItem toMove, IModularItem target) { + if (target == null) return false; + var isDeleted = false; + + if (target is IModularContainer container && !container.Items.Any()) { + // if target is an empty container/splitter + // drop it inside + + isDeleted |= Delete(toMove); + + // add it in the items of target + ((IModularContainer)target).Items.Add(toMove); + + } else { + // if it's a panel or a filled container/splitter + // drop it above + + isDeleted |= Delete(toMove); + InsertConfigBefore(toMove, target); + } + Manager.Init(); + return isDeleted; + } + private static void InsertConfigBefore(IModularItem itemtoInsert, IModularItem itemBefore) { + InsertBefore(Ktisis.Configuration.ModularConfig, itemtoInsert, itemBefore); + } + + private static void InsertBefore(List? items, IModularItem itemtoInsert, IModularItem itemBefore) { + if (items == null || !items.Any()) return; + + int index = items.FindIndex(r => r == itemBefore); + if (index > -1) + items.Insert(index, itemtoInsert); + + items.ForEach(co => InsertBefore(co is IModularContainer container ? container.Items : null, itemtoInsert, itemBefore)); + } + private static void MoveSource(IModularItem source) => + MovingItem = source; + + private static bool MoveTarget(IModularItem target) { + if (MovingItem == null) return false; + var movingTaget = MovingItem; + MovingItem = null; + return MoveAt(movingTaget, target); + } + + private unsafe static bool TreeNode(IModularItem item) { + bool isLeaf = !(item is IModularContainer container && container.Items.Any()); + + string handle = item.GetType().Name; + string id = item.GetHashCode().ToString(); + + bool open = ImGui.TreeNodeEx(id, ImGuiTreeNodeFlags.FramePadding | ImGuiTreeNodeFlags.DefaultOpen | (item == SelectedItem ? ImGuiTreeNodeFlags.Selected : 0) | (isLeaf ? ImGuiTreeNodeFlags.Leaf : ImGuiTreeNodeFlags.OpenOnArrow), handle); + bool iteratorModified = false; + ImGui.PushID(id); + if (ImGui.BeginPopupContextItem()) { + iteratorModified |= DrawContextMenu(item); + ImGui.EndPopup(); + } + ImGui.PopID(); + + if (ImGui.IsItemClicked()) { + SelectedItem = item; + } + if (ImGui.IsItemClicked() && ImGui.GetIO().KeyCtrl && ImGui.GetIO().KeyShift) + iteratorModified |= Delete(item); + + if (ImGui.BeginDragDropTarget()) { + + // highlight target + ImGui.AcceptDragDropPayload("_IModularItem"); + + // Small hack to fire MoveTarget() on mouse button release + if (MovingItem != null && !ImGui.GetIO().MouseDown[(int)ImGuiMouseButton.Left]) + iteratorModified |= MoveTarget(item); + + ImGui.EndDragDropTarget(); + } + + if (ImGui.BeginDragDropSource()) { + ImGui.SetDragDropPayload("_IModularItem", IntPtr.Zero, 0); + + // Small hack to set the move source on mouse button hold + if (ImGui.GetIO().MouseDownDuration[(int)ImGuiMouseButton.Left] < 0.5f) + MoveSource(item); + + ImGui.EndDragDropSource(); + } + + + if (open) { + // Recursive call... + if (!isLeaf && !iteratorModified) + foreach (var child in ((IModularContainer)item).Items) { + iteratorModified |= TreeNode(child); + if (iteratorModified) + break; + } + + ImGui.TreePop(); + } + return iteratorModified; + } + private static bool DrawContextMenu(IModularItem item) { + SelectedItem = item; + if (GuiHelpers.IconButtonTooltip(FontAwesomeIcon.Plus, $"Add above {item.GetType().Name}")) + IsAddPanelOpen = AddItemListMode.AboveSelected; + + ImGui.SameLine(); + if (GuiHelpers.IconButtonHoldConfirm(FontAwesomeIcon.Trash, $"Delete {item.GetType().Name}")) + if (Delete(item)) + return true; + + return false; + } + private static void DrawItemDetails(IModularItem? item) { + if (item == null) return; + item.DrawConfig(); + } + + internal static bool DrawBitwiseFlagSelect(string label, ref TEnum flagStack) where TEnum : struct, Enum => + DrawBitwiseFlagSelect(label, ref flagStack, Enum.GetValues().ToList()); + internal static bool DrawBitwiseFlagSelect(string label, ref TEnum flagStack, List whitelist) where TEnum : struct, Enum => + DrawFlagSelect(label, ref flagStack, whitelist, true); + internal static bool DrawFlagSelect(string label, ref TEnum flagStack) where TEnum : struct, Enum => + DrawFlagSelect(label, ref flagStack, Enum.GetValues().ToList(), false); + internal static bool DrawFlagSelect(string label, ref TEnum flagStack, List whitelist) where TEnum : struct, Enum => + DrawFlagSelect(label, ref flagStack, whitelist, false); + internal static bool DrawFlagSelect(string label, ref TEnum flagStack, List whitelist, bool bitwise) where TEnum : struct, Enum { + if (!ImGui.CollapsingHeader(label)) return false; + + ImGui.TextWrapped(flagStack.ToString()); + + bool active = false; + int intFlagStack = Convert.ToInt32(flagStack); + active |= ImGui.InputInt($"##{label}##DrawFlagSelect##IntInput", ref intFlagStack, 1, 10, ImGuiInputTextFlags.EnterReturnsTrue); + + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.BeginListBox($"##{label}##DrawFlagSelect")) { + + foreach (var flag in whitelist.Cast()) { + + bool hasFlag = bitwise? (intFlagStack & flag) != 0 : intFlagStack == flag; + if (ImGui.Selectable($"{(TEnum)Enum.ToObject(typeof(TEnum), flag)}##Modular##Details##{typeof(TEnum).Name}", hasFlag)) { + active |= true; + if (bitwise) { + if (!hasFlag) + intFlagStack |= flag; + else + intFlagStack &= ~flag; + } else { + intFlagStack = flag; + } + } + } + } + ImGui.EndListBox(); + if (active) + flagStack = (TEnum)Enum.ToObject(typeof(TEnum), intFlagStack); + return active; + } + } +} \ No newline at end of file diff --git a/Ktisis/Interface/Modular/IModularItem.cs b/Ktisis/Interface/Modular/IModularItem.cs new file mode 100644 index 000000000..aa88825b8 --- /dev/null +++ b/Ktisis/Interface/Modular/IModularItem.cs @@ -0,0 +1,55 @@ +using Newtonsoft.Json.Linq; +using Newtonsoft.Json; + +using System; +using System.Collections.Generic; + +namespace Ktisis.Interface.Modular { + + [JsonConverter(typeof(IModularItemConverter))] + public interface IModularItem { + public string GetTitle(); + public void DrawConfig(); + public void Draw(); + } + public interface IModularContainer : IModularItem { + public List Items { get; } + } + + + public class IModularItemConverter : JsonConverter { + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { + throw new NotImplementedException(); + } + + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { + + var jo = JObject.Load(reader); + + var typeString = jo["$type"]?.ToString(); + if (typeString == null) + throw new JsonReaderException(); + + var type = Type.GetType(typeString); + if (type == null) + throw new JsonReaderException(); + + var item = Manager.CreateItemFromTypeName(type.Name); + if (item == null) + throw new JsonReaderException(); + + serializer.Populate(jo.CreateReader(), item); + + return item; + } + + public override bool CanWrite { + get { return false; } + } + + public override bool CanConvert(Type objectType) { + return false; + } + } +} diff --git a/Ktisis/Interface/Modular/ItemTypes/BaseItems.cs b/Ktisis/Interface/Modular/ItemTypes/BaseItems.cs new file mode 100644 index 000000000..d4353c26d --- /dev/null +++ b/Ktisis/Interface/Modular/ItemTypes/BaseItems.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using System.Linq; + +using ImGuiNET; +using Newtonsoft.Json; + +using Ktisis.Localization; + +namespace Ktisis.Interface.Modular.ItemTypes { + + public class BaseItem : IModularItem { + protected int Id; + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string? Title { get; set; } + [JsonIgnore] + public string LocaleHandle { get; set; } + + public BaseItem() { + Id = GenerateId(); + Title = null; + LocaleHandle = "ModularItem"; + } + private static int GenerateId() { + int id = 0; + if (Manager.ItemIds.Any()) + id = Manager.ItemIds.Max() + 1; + Manager.ItemIds.Add(id); + return id; + } + + virtual public void DrawConfig() { + string title = Title ?? ""; + if (ImGui.InputText($"Title##ModularItem#Field", ref title, 200)) + Title = title; + } + + virtual public string LocaleName() => Locale.GetString(this.LocaleHandle); + virtual public string GetTitle() => $"{this.Title ?? this.LocaleName()}##Modular##Item##{this.Id}"; + + // virtual methods + virtual public void Draw() { } + + } + public class BaseContainer : BaseItem, IModularContainer { + public List Items { get; set; } = new(); + public BaseContainer() : base() { } + + override public void Draw() { + if (this.Items != null) + foreach (var item in this.Items) { + this.DrawItem(item); + } + } + virtual protected void DrawItem(IModularItem item) => item.Draw(); + } + + public class BaseSplitter : BaseItem, IModularContainer { + public List Items { get; set; } = new(); + + override public void Draw() { + if (this.Items != null) + foreach (var item in this.Items) { + this.DrawItem(item); + } + } + + virtual protected void DrawItem(IModularItem item) => item.Draw(); + } + + public class BasePannel : BaseItem { + } + + + + ///////////////// + // Items ideas // + ///////////////// + + // indicator for bone cat overload (with initials maybe) + // individual buttons for siblings and ControlButton extras + // window open/toggle => it would be able to have containers as children + // Sameline X space configurable (float) + + /////////////////// + // Concept ideas // + /////////////////// + + // force fix width to prevent align right + // improve drag drop + +} diff --git a/Ktisis/Interface/Modular/ItemTypes/Container.cs b/Ktisis/Interface/Modular/ItemTypes/Container.cs new file mode 100644 index 000000000..f0746b51e --- /dev/null +++ b/Ktisis/Interface/Modular/ItemTypes/Container.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.Numerics; + +using ImGuiNET; + +namespace Ktisis.Interface.Modular.ItemTypes.Container { + + public class Window : BaseContainer { + public ImGuiWindowFlags WindowFlags { get; set; } + public Window() : base() { } + + private static readonly List AllowedWindowFlags = new() { + ImGuiWindowFlags.None, + ImGuiWindowFlags.NoTitleBar, + ImGuiWindowFlags.NoResize, + ImGuiWindowFlags.NoMove, + ImGuiWindowFlags.NoScrollbar, + ImGuiWindowFlags.NoCollapse, + ImGuiWindowFlags.NoDecoration, + ImGuiWindowFlags.AlwaysAutoResize, + ImGuiWindowFlags.NoBackground, + ImGuiWindowFlags.NoSavedSettings, + ImGuiWindowFlags.NoMouseInputs, + ImGuiWindowFlags.HorizontalScrollbar, + ImGuiWindowFlags.NoFocusOnAppearing, + ImGuiWindowFlags.NoBringToFrontOnFocus, + ImGuiWindowFlags.AlwaysVerticalScrollbar, + ImGuiWindowFlags.AlwaysHorizontalScrollbar, + ImGuiWindowFlags.AlwaysUseWindowPadding, + }; + + public Window(ImGuiWindowFlags windowFlags) : this() { + this.WindowFlags = windowFlags; + } + override public void Draw() { + if (ImGui.Begin(this.GetTitle(), this.WindowFlags)) { + if (this.Items != null) + foreach (var item in this.Items) { + this.DrawItem(item); + } + } + ImGui.End(); + } + public override void DrawConfig() { + base.DrawConfig(); + var windowFlags = WindowFlags; + Configurator.DrawBitwiseFlagSelect("Window Flags", ref windowFlags, AllowedWindowFlags); + WindowFlags = windowFlags; + } + } + + + public class ResizableWindow : Window { } + public class WindowAutoResize : Window { + public WindowAutoResize() : base(ImGuiWindowFlags.AlwaysAutoResize) { } + } + public class BarWindow : Window { + public BarWindow() : base(ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.AlwaysAutoResize) { } + protected override void DrawItem(IModularItem item) { + ImGui.SameLine(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (ImGui.GetStyle().ItemSpacing.X * 2)); + base.DrawItem(item); + } + } + public class BorderWindow : Window { + enum WindowLocation { Top, Bottom, Left, Right } + WindowLocation Location { get; set; } = WindowLocation.Top; + float ReverseOffset = -100; + public BorderWindow() : base(ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.AlwaysAutoResize) { } + public override void Draw() { + + switch (Location) { + case WindowLocation.Top: + ImGui.SetNextWindowPos(Vector2.Zero); + ImGui.SetNextWindowSize(new(ImGui.GetIO().DisplaySize.X, -1)); + break; + case WindowLocation.Bottom: + ImGui.SetNextWindowPos(new(0, ImGui.GetIO().DisplaySize.Y - ReverseOffset)); + ImGui.SetNextWindowSize(new(ImGui.GetIO().DisplaySize.X, -1)); + break; + case WindowLocation.Left: + ImGui.SetNextWindowPos(new(0)); + ImGui.SetNextWindowSize(new(-1, ImGui.GetIO().DisplaySize.Y)); + break; + case WindowLocation.Right: + ImGui.SetNextWindowPos(new(ImGui.GetIO().DisplaySize.X - ReverseOffset, 0)); + ImGui.SetNextWindowSize(new(-1, ImGui.GetIO().DisplaySize.Y)); + break; + default: + break; + } + ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0); + ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0); + if (ImGui.Begin(this.GetTitle(), this.WindowFlags)) { + if(Location == WindowLocation.Bottom) ReverseOffset = ImGui.GetWindowSize().Y; + if(Location == WindowLocation.Right) ReverseOffset = ImGui.GetWindowSize().X; + if (this.Items != null) + foreach (var item in this.Items) { + this.DrawItem(item); + } + } + ImGui.End(); + ImGui.PopStyleVar(2); + } + protected override void DrawItem(IModularItem item) { + if (Location == WindowLocation.Bottom || Location == WindowLocation.Top) { + ImGui.SameLine(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (ImGui.GetStyle().ItemSpacing.X * 2)); + } + base.DrawItem(item); + } + public override void DrawConfig() { + base.DrawConfig(); + var location = Location; + Configurator.DrawFlagSelect("Location", ref location); + Location = location; + } + + } +} \ No newline at end of file diff --git a/Ktisis/Interface/Modular/ItemTypes/Panels.cs b/Ktisis/Interface/Modular/ItemTypes/Panels.cs new file mode 100644 index 000000000..91ec71dc5 --- /dev/null +++ b/Ktisis/Interface/Modular/ItemTypes/Panels.cs @@ -0,0 +1,73 @@ +using ImGuiNET; + +using Ktisis.Interface.Components; +using Ktisis.Interface.Windows.ActorEdit; +using Ktisis.Interface.Windows.Workspace; + +namespace Ktisis.Interface.Modular.ItemTypes.Panel { + public class ActorList : BasePannel { + public ActorList() : base() => LocaleHandle = "Actor List"; + public override void Draw() => ActorsList.Draw(); + } + public class ActorListHorizontal : BasePannel { + public ActorListHorizontal() : base() => LocaleHandle = "Actor List"; + public override void Draw() => ActorsList.Draw(true); + } + public class ControlButtonsExtra : BasePannel { + public override void Draw() => ControlButtons.DrawExtra(); + } + public class SettingsButton : BasePannel { + public override void Draw() => ControlButtons.DrawSettings(0); + } + public class HandleEmpty : BasePannel { + public override void Draw() => ImGui.Text(" "); + } + public class GizmoOperations : BasePannel { + public override void Draw() => ControlButtons.DrawGizmoOperations(); + } + public class GposeTextIndicator : BasePannel { + public override void Draw() => Workspace.DrawGposeIndicator(); + } + public class PoseSwitch : BasePannel { + public override void Draw() => ControlButtons.DrawPoseSwitch(); + } + public class SelectInfo : BasePannel { + public override void Draw() => Workspace.SelectInfo(); + } + public class EditActorButton : BasePannel { + public override void Draw() => EditActor.DrawButton(); + } + public class AnimationControls : BasePannel { + public AnimationControls() : base() => LocaleHandle = "Animation Control"; + + public override void Draw() => Components.AnimationControls.Draw(); + } + public class GazeControl : BasePannel { + public GazeControl() : base() => LocaleHandle = "Gaze Control"; + + public override void Draw() => EditGaze.DrawWithHint(); + } + public class ParentingCheckbox : BasePannel { + public override void Draw() => ControlButtons.DrawParentingCheckbox(); + } + public class TransformTable : BasePannel { + public override void Draw() => Workspace.TransformTable(); + } + public class CategoryVisibility : BasePannel { + public CategoryVisibility() : base() => LocaleHandle = "Bone Categories"; + public override void Draw() => Categories.DrawToggleListWithHint(); + } + public class BoneTree : BasePannel { + public BoneTree() : base() => LocaleHandle = "Bone List"; + public override void Draw() => Components.BoneTree.Draw(); + } + public class ImportExport : BasePannel { + public ImportExport() : base() => LocaleHandle = "Import & Export"; + public override void Draw() => Workspace.DrawImportExport(); + } + public class Advanced : BasePannel { + public Advanced() : base() => LocaleHandle = "Advanced"; + public override void Draw() => Workspace.DrawAdvanced(); + } + +} diff --git a/Ktisis/Interface/Modular/ItemTypes/Splitters.cs b/Ktisis/Interface/Modular/ItemTypes/Splitters.cs new file mode 100644 index 000000000..1eb738c80 --- /dev/null +++ b/Ktisis/Interface/Modular/ItemTypes/Splitters.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; + +using ImGuiNET; + +namespace Ktisis.Interface.Modular.ItemTypes.Splitter { + public class Columns : BaseSplitter { + public override void Draw() { + if (this.Items != null) { + ImGui.Columns(this.Items.Count); + foreach (var item in this.Items) { + this.DrawItem(item); + ImGui.NextColumn(); + } + ImGui.Columns(); + } + } + } + + public class BorderlessColumns : BaseSplitter { + public override void Draw() { + if (this.Items != null) { + ImGui.Columns(this.Items.Count, this.Title, false); + foreach (var item in this.Items) { + this.DrawItem(item); + ImGui.NextColumn(); + } + ImGui.Columns(); + } + } + } + + + public class SameLine : BaseSplitter { + public override void Draw() { + if (this.Items != null) { + for (int i = 0; i < this.Items.Count; i++) { + if (i != 0) ImGui.SameLine(); + this.DrawItem(this.Items[i]); + } + } + } + } + public class CollapsibleHeader : BaseSplitter { + public override void Draw() { + if (this.Items != null) + for (int i = 0; i < this.Items.Count; i++) + if (ImGui.CollapsingHeader(this.Items[i].GetTitle())) + this.DrawItem(this.Items[i]); + } + } + public class Tabs : BaseSplitter { + public override void Draw() { + if (this.Items != null) + if (ImGui.BeginTabBar(GetTitle())) + for (int i = 0; i < this.Items.Count; i++) + if (ImGui.BeginTabItem(this.Items[i].GetTitle())) { + this.DrawItem(this.Items[i]); + ImGui.EndTabItem(); + } + + } + } + + public class Group : BaseSplitter { } +} diff --git a/Ktisis/Interface/Modular/Manager.cs b/Ktisis/Interface/Modular/Manager.cs new file mode 100644 index 000000000..786b93cce --- /dev/null +++ b/Ktisis/Interface/Modular/Manager.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +using Newtonsoft.Json; + +namespace Ktisis.Interface.Modular { + internal class Manager { + + private const string NamespacePrefix = "Ktisis.Interface.Modular.ItemTypes."; + + internal static readonly List AvailableContainers = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.Namespace == NamespacePrefix + "Container").ToList(); + internal static readonly List AvailableSpliters = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.Namespace == NamespacePrefix + "Splitter").ToList(); + internal static readonly List AvailablePanel = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.Namespace == NamespacePrefix + "Panel").ToList(); + internal static readonly List Available = AvailableContainers.Concat(AvailableSpliters).Concat(AvailablePanel).ToList(); + + public static List ItemIds = new(); + + public static void Init() { + Configurator.MovingItem = null; + } + public static void Dispose() { + ItemIds.Clear(); + } + public static void Render() => Ktisis.Configuration.ModularConfig.ForEach(d => d.Draw()); + public static int GenerateId() { + int id = 0; + if (ItemIds.Any()) + id = ItemIds.Max() + 1; + ItemIds.Add(id); + return id; + } + internal static IModularItem? CreateItemFromTypeName(string typeName) { + + // Get the type of desired instance + Type? objectType = Available.FirstOrDefault(i => i.Name == typeName); + if (objectType == null) return null; + + // Get available constructors + var constructors = objectType.GetConstructors(BindingFlags.Instance | BindingFlags.Public); + if (constructors == null || constructors.Length == 0) return null; + + object? instance = null; + + // if parameterless constructor exists, use it + if (constructors.Any(c => c.GetParameters().Length == 0)) { + instance = Activator.CreateInstance(objectType); + if (instance != null) return (IModularItem)instance; + } + return null; + } + } +} diff --git a/Ktisis/Interface/Windows/ActorEdit/EditActor.cs b/Ktisis/Interface/Windows/ActorEdit/EditActor.cs index eb9bcf0cc..f5e861202 100644 --- a/Ktisis/Interface/Windows/ActorEdit/EditActor.cs +++ b/Ktisis/Interface/Windows/ActorEdit/EditActor.cs @@ -1,6 +1,8 @@ using System.Numerics; using ImGuiNET; +using Dalamud.Interface; +using Dalamud.Interface.Components; using Ktisis.Structs.Actor; @@ -17,9 +19,16 @@ public unsafe static Actor* Target public static void Show() => Visible = true; public static void Hide() => Visible = false; + public static void Toggle() => Visible = !Visible; // Display + public static void DrawButton() { + if (ImGuiComponents.IconButton(FontAwesomeIcon.UserEdit)) + Toggle(); + ImGui.SameLine(); + ImGui.Text("Edit actor's appearance"); + } public unsafe static void Draw() { if (!Visible) return; diff --git a/Ktisis/Interface/Windows/ConfigGui.cs b/Ktisis/Interface/Windows/ConfigGui.cs index 88057ced8..1d53f907f 100644 --- a/Ktisis/Interface/Windows/ConfigGui.cs +++ b/Ktisis/Interface/Windows/ConfigGui.cs @@ -2,12 +2,9 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; -using System.Text; using ImGuiNET; -using Newtonsoft.Json; - using Dalamud.Logging; using Dalamud.Interface; using Dalamud.Interface.Components; @@ -64,6 +61,7 @@ public static void Draw() { DrawLanguageTab(cfg); if (ImGui.BeginTabItem("Data")) DrawDataTab(cfg); + Modular.Configurator.DrawConfigTab(cfg); ImGui.EndTabBar(); } @@ -375,6 +373,15 @@ public static void DrawDataTab(Configuration cfg) { Sets.Dispose(); cfg.GlamourPlateData = null; } + + ImGui.Separator(); + ImGui.Spacing(); + + var clipboardExportClearJson = cfg.ClipboardExportClearJson; + if (ImGui.Checkbox($"Clipboard exports clear json", ref clipboardExportClearJson)) { + cfg.ClipboardExportClearJson = clipboardExportClearJson; + } + ImGui.EndTabItem(); } @@ -406,10 +413,10 @@ public static unsafe void DrawBonesOffset(Configuration cfg) { cfg.CustomBoneOffset.Remove(bodyType); ImGui.SameLine(); if (GuiHelpers.IconButton(FontAwesomeIcon.Clipboard, default, $"export##{bodyType}")) - ImGui.SetClipboardText(Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(cfg.CustomBoneOffset[bodyType])))); + JsonHelpers.ExportClipboard(cfg.CustomBoneOffset[bodyType]); ImGui.SameLine(); if (GuiHelpers.IconButtonHoldConfirm(FontAwesomeIcon.Paste, $"Hold Ctrl and Shift to paste and replace all {bodyType} bone offsets.", default, $"pasteReplaceAll##{bodyType}")) { - var parsedPasteAll = JsonConvert.DeserializeObject>(Encoding.UTF8.GetString(Convert.FromBase64String(ImGui.GetClipboardText()))); + var parsedPasteAll = JsonHelpers.ImportClipboard>(); if (parsedPasteAll != null) cfg.CustomBoneOffset[bodyType] = parsedPasteAll; } @@ -437,7 +444,7 @@ public static unsafe void DrawBonesOffset(Configuration cfg) { var isDeletable = ImGui.GetIO().KeyCtrl && ImGui.GetIO().KeyShift; if(isDeletable) ImGui.PushStyleColor(ImGuiCol.HeaderHovered, Workspace.Workspace.ColRed); if (ImGui.Selectable($"{Locale.GetBoneName(boneName)}##{bodyType}##customBoneOffset", false, ImGuiSelectableFlags.SpanAllColumns)) - ImGui.SetClipboardText(Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject((boneName,offsets))))); + JsonHelpers.ExportClipboard((boneName, offsets)); if (isDeletable) ImGui.PopStyleColor(); if (isDeletable && ImGui.IsItemClicked(ImGuiMouseButton.Right)) cfg.CustomBoneOffset[bodyType].Remove(boneName); @@ -450,7 +457,7 @@ public static unsafe void DrawBonesOffset(Configuration cfg) { } ImGui.EndTable(); if (GuiHelpers.IconButtonTooltip(FontAwesomeIcon.Plus,"Add a line from clipboard.", default, $"{bodyType}##Clipboard##AddLine")) { - var parsedPasteLine = JsonConvert.DeserializeObject<(string, Vector3)>(Encoding.UTF8.GetString(Convert.FromBase64String(ImGui.GetClipboardText()))) ; + var parsedPasteLine = JsonHelpers.ImportClipboard<(string, Vector3)>(); cfg.CustomBoneOffset[bodyType][parsedPasteLine.Item1] = parsedPasteLine.Item2; } } diff --git a/Ktisis/Interface/Windows/Toolbar/AdvancedWindow.cs b/Ktisis/Interface/Windows/Toolbar/AdvancedWindow.cs index b5a822437..31bb277ea 100644 --- a/Ktisis/Interface/Windows/Toolbar/AdvancedWindow.cs +++ b/Ktisis/Interface/Windows/Toolbar/AdvancedWindow.cs @@ -41,14 +41,14 @@ public unsafe static void Draw() { if (actor->Model != null) { // Animation Controls - AnimationControls.Draw(target); + AnimationControls.Draw(); // Gaze Controls if (ImGui.CollapsingHeader("Gaze Control")) { if (PoseHooks.PosingEnabled) ImGui.TextWrapped("Gaze controls are unavailable while posing."); else - EditGaze.Draw(actor); + EditGaze.Draw(); } // Advanced diff --git a/Ktisis/Interface/Windows/Toolbar/BonesWindow.cs b/Ktisis/Interface/Windows/Toolbar/BonesWindow.cs index 473de4f60..4c5daa208 100644 --- a/Ktisis/Interface/Windows/Toolbar/BonesWindow.cs +++ b/Ktisis/Interface/Windows/Toolbar/BonesWindow.cs @@ -31,8 +31,6 @@ public unsafe static void Draw() { if (ImGui.Begin("Bones", ref Visible, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.AlwaysAutoResize)) { var cfg = Ktisis.Configuration; - var target = Ktisis.GPoseTarget; - var actor = (Actor*)target!.Address; // Bone categories if (!Categories.DrawToggleList(cfg)) { @@ -45,7 +43,7 @@ public unsafe static void Draw() { } // Bone tree - BoneTree.Draw(actor); + BoneTree.Draw(); } ImGui.PopStyleVar(); diff --git a/Ktisis/Interface/Windows/Workspace/EditGaze.cs b/Ktisis/Interface/Windows/Workspace/EditGaze.cs index f435d5548..266e9ca3b 100644 --- a/Ktisis/Interface/Windows/Workspace/EditGaze.cs +++ b/Ktisis/Interface/Windows/Workspace/EditGaze.cs @@ -6,6 +6,7 @@ using Dalamud.Interface; using Dalamud.Interface.Components; +using Ktisis.Interop.Hooks; using Ktisis.Overlay; using Ktisis.Structs.Actor; using Ktisis.Structs.Extensions; @@ -23,8 +24,16 @@ public static bool IsLinked { public const uint UsingColor = 0xffde851f; // UI Code + public unsafe static void DrawWithHint() { + if (PoseHooks.PosingEnabled) + ImGui.TextWrapped("Gaze controls are unavailable while posing."); + else + EditGaze.Draw(); + } + public unsafe static void Draw() { + var target = Ktisis.Target; + if (target == null) return; - public unsafe static void Draw(Actor* target) { if (ActorControl == null) ActorControl = new(); diff --git a/Ktisis/Interface/Windows/Workspace/Workspace.cs b/Ktisis/Interface/Windows/Workspace/Workspace.cs index d8623cd44..17db55656 100644 --- a/Ktisis/Interface/Windows/Workspace/Workspace.cs +++ b/Ktisis/Interface/Windows/Workspace/Workspace.cs @@ -53,7 +53,9 @@ public static void Draw() { if (!Visible) return; - var gposeOn = Ktisis.IsInGPose; + DrawExtraWindows(); + if (Ktisis.Configuration.ModularHideDefaultWorkspace) return; + var size = new Vector2(-1, -1); ImGui.SetNextWindowSize(size, ImGuiCond.FirstUseEver); @@ -65,12 +67,7 @@ public static void Draw() { ControlButtons.PlaceAndRenderSettings(); ImGui.BeginGroup(); - ImGui.AlignTextToFramePadding(); - - ImGui.TextColored( - gposeOn ? ColGreen : ColRed, - gposeOn ? "GPose Enabled" : "GPose Disabled" - ); + DrawGposeIndicator(); if (PoseHooks.AnamPosingEnabled) { ImGui.TextColored( @@ -91,7 +88,7 @@ public static void Draw() { // Selection info ImGui.Spacing(); - SelectInfo(target); + SelectInfo(); // Actor control @@ -112,6 +109,27 @@ public static void Draw() { ImGui.End(); } + private static void DrawExtraWindows() { + + // Draw Modular UI + if (Ktisis.Configuration.ModularConfig != null) + Modular.Manager.Render(); + + // Draw Actor list to avoir duplication from double instance in modular UI + if (ActorsList.SelectorList != null) + ActorsList.DrawListAddActor(); + } + + public static void DrawGposeIndicator() { + ImGui.AlignTextToFramePadding(); + + var gposeOn = Ktisis.IsInGPose; + ImGui.TextColored( + gposeOn ? ColGreen : ColRed, + gposeOn ? "GPose Enabled" : "GPose Disabled" + ); + } + // Actor tab (Real) private unsafe static void ActorTab(GameObject target) { @@ -127,30 +145,21 @@ private unsafe static void ActorTab(GameObject target) { ImGui.Spacing(); // Customize button - if (ImGuiComponents.IconButton(FontAwesomeIcon.UserEdit)) { - if (EditActor.Visible) - EditActor.Hide(); - else - EditActor.Show(); - } - ImGui.SameLine(); - ImGui.Text("Edit actor's appearance"); + EditActor.DrawButton(); ImGui.Spacing(); // Actor list - ActorsList.Draw(); + if (ImGui.CollapsingHeader("Actor List")) + ActorsList.Draw(); // Animation control - AnimationControls.Draw(target); + if (ImGui.CollapsingHeader("Animation Control")) + AnimationControls.Draw(); // Gaze control - if (ImGui.CollapsingHeader("Gaze Control")) { - if (PoseHooks.PosingEnabled) - ImGui.TextWrapped("Gaze controls are unavailable while posing."); - else - EditGaze.Draw(actor); - } + if (ImGui.CollapsingHeader("Gaze Control")) + EditGaze.DrawWithHint(); // Import & Export if (ImGui.CollapsingHeader("Import & Export")) @@ -175,60 +184,78 @@ private unsafe static void PoseTab(GameObject target) { ControlButtons.DrawExtra(); // Parenting - - var parent = cfg.EnableParenting; - if (ImGui.Checkbox("Parenting", ref parent)) - cfg.EnableParenting = parent; + ControlButtons.DrawParentingCheckbox(); // Transform table - TransformTable(actor); + TransformTable(); ImGui.Spacing(); // Bone categories - if (ImGui.CollapsingHeader("Bone Categories")) { - - if (!Categories.DrawToggleList(cfg)) { - ImGui.Text("No bone found."); - ImGui.Text("Show Skeleton ("); - ImGui.SameLine(); - GuiHelpers.Icon(FontAwesomeIcon.EyeSlash); - ImGui.SameLine(); - ImGui.Text(") to fill this."); - } - } + if (ImGui.CollapsingHeader("Bone Categories")) + Categories.DrawToggleListWithHint(); // Bone tree - BoneTree.Draw(actor); + if (ImGui.CollapsingHeader("Bone List")) + BoneTree.Draw(); // Import & Export if (ImGui.CollapsingHeader("Import & Export")) - ImportExportPose(actor); + DrawImportExport(); // Advanced - if (ImGui.CollapsingHeader("Advanced (Debug)")) { - if (ImGui.Button("Reset Current Pose") && actor->Model != null) - actor->Model->SyncModelSpace(); + if (ImGui.CollapsingHeader("Advanced (Debug)")) + DrawAdvanced(); - if (ImGui.Button("Set to Reference Pose") && actor->Model != null) - actor->Model->SyncModelSpace(true); + ImGui.EndTabItem(); + } - if (ImGui.Button("Store Pose") && actor->Model != null) - _TempPose.Store(actor->Model->Skeleton); - ImGui.SameLine(); - if (ImGui.Button("Apply Pose") && actor->Model != null) - _TempPose.Apply(actor->Model->Skeleton); + internal static void DrawImportExport() { + ImGui.Text("Transforms"); + var _ = false; + ImGui.Checkbox("R", ref _); + ImGui.SameLine(); + ImGui.Checkbox("P", ref _); + ImGui.SameLine(); + ImGui.Checkbox("S", ref _); - if (ImGui.Button("Force Redraw")) - actor->Redraw(); - } + ImGui.Checkbox("Body", ref _); + ImGui.SameLine(); + ImGui.Checkbox("Expression", ref _); - ImGui.EndTabItem(); + ImGui.Spacing(); + + ImGui.Button("Import"); + ImGui.SameLine(); + ImGui.Button("Export"); + + ImGui.Spacing(); + } + internal unsafe static void DrawAdvanced() { + var actor = Ktisis.Target; + if (actor->Model == null) return; + + if (ImGui.Button("Reset Current Pose") && actor->Model != null) + actor->Model->SyncModelSpace(); + + if (ImGui.Button("Set to Reference Pose") && actor->Model != null) + actor->Model->SyncModelSpace(true); + + if (ImGui.Button("Store Pose") && actor->Model != null) + _TempPose.Store(actor->Model->Skeleton); + ImGui.SameLine(); + if (ImGui.Button("Apply Pose") && actor->Model != null) + _TempPose.Apply(actor->Model->Skeleton); + + if (ImGui.Button("Force Redraw")) + actor->Redraw(); } // Transform Table actor and bone names display, actor related extra - private static unsafe bool TransformTable(Actor* target) { + internal static unsafe bool TransformTable() { + var target = Ktisis.Target; + if (target == null) return false; var select = Skeleton.BoneSelect; var bone = Skeleton.GetSelectedBone(); @@ -240,8 +267,9 @@ private static unsafe bool TransformTable(Actor* target) { // Selection details - private unsafe static void SelectInfo(GameObject target) { - var actor = (Actor*)target.Address; + internal unsafe static void SelectInfo() { + var actor = Ktisis.Target; + if (actor == null) return; var select = Skeleton.BoneSelect; var bone = Skeleton.GetSelectedBone(); diff --git a/Ktisis/Ktisis.cs b/Ktisis/Ktisis.cs index e9ee89ee7..9f4904b4c 100644 --- a/Ktisis/Ktisis.cs +++ b/Ktisis/Ktisis.cs @@ -47,6 +47,7 @@ public Ktisis(DalamudPluginInterface pluginInterface) { Configuration.Validate(); + Interface.Modular.Manager.Init(); // Init interop stuff Interop.Alloc.Init(); @@ -100,6 +101,8 @@ public void Dispose() { ActorStateWatcher.Instance.Dispose(); EventManager.OnGPoseChange -= Workspace.OnEnterGposeToggle; + Interface.Modular.Manager.Dispose(); + Data.Sheets.Cache.Clear(); if (EditEquip.Items != null) diff --git a/Ktisis/Util/IconsPool.cs b/Ktisis/Util/IconsPool.cs index 9a5fd5e5d..5ef58bb7b 100644 --- a/Ktisis/Util/IconsPool.cs +++ b/Ktisis/Util/IconsPool.cs @@ -5,6 +5,7 @@ public static class IconsPool { public const FontAwesomeIcon Position = FontAwesomeIcon.LocationArrow; public const FontAwesomeIcon Rotation = FontAwesomeIcon.Sync; public const FontAwesomeIcon Scale = FontAwesomeIcon.ExpandAlt; + public const FontAwesomeIcon Universal = FontAwesomeIcon.Circle; public const FontAwesomeIcon Settings = FontAwesomeIcon.Cog; public const FontAwesomeIcon More = FontAwesomeIcon.Bars; public const FontAwesomeIcon UserEdit = FontAwesomeIcon.UserEdit; diff --git a/Ktisis/Util/Misc.cs b/Ktisis/Util/Misc.cs new file mode 100644 index 000000000..f557de87b --- /dev/null +++ b/Ktisis/Util/Misc.cs @@ -0,0 +1,76 @@ +using System; +using System.IO.Compression; +using System.IO; +using System.Text; + +using ImGuiNET; +using Newtonsoft.Json; + +namespace Ktisis.Util { + internal class JsonHelpers { + + + public static void ExportClipboard(object? objectToExport) { + var str = JsonConvert.SerializeObject(objectToExport, objectToExport?.GetType(), + new JsonSerializerSettings { + TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, + TypeNameHandling = TypeNameHandling.Objects, + }); + if (!Ktisis.Configuration.ClipboardExportClearJson) + str = Convert.ToBase64String(CompressString(str)); + ImGui.SetClipboardText(str); + } + public static T? ImportClipboard() { + var rawString = ImGui.GetClipboardText(); + if (rawString == null) return default; + + if (IsBase64String(rawString)) { + var base64String = Convert.FromBase64String(rawString); + + try { + var compressedMaybe = JsonConvert.DeserializeObject(DecompressString(base64String)); + if (compressedMaybe != null) + return compressedMaybe; + } catch (Exception) { } + try { + var base64Maybe = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(base64String)); + if (base64Maybe != null) + return base64Maybe; + } catch (Exception) { } + } + + try { + return JsonConvert.DeserializeObject(rawString); + } catch (JsonReaderException) { + return default; + } + } + + public static bool IsBase64String(string base64) { + Span buffer = new Span(new byte[base64.Length]); + return Convert.TryFromBase64String(base64, buffer, out int bytesParsed); + } + + public static byte[] CompressString(string str) { + var bytes = Encoding.UTF8.GetBytes(str); + + using (var msi = new MemoryStream(bytes)) + using (var mso = new MemoryStream()) { + using (var gs = new GZipStream(mso, CompressionMode.Compress)) + msi.CopyTo(gs); + + return mso.ToArray(); + } + } + + public static string DecompressString(byte[] bytes) { + using (var msi = new MemoryStream(bytes)) + using (var mso = new MemoryStream()) { + using (var gs = new GZipStream(msi, CompressionMode.Decompress)) + gs.CopyTo(mso); + + return Encoding.UTF8.GetString(mso.ToArray()); + } + } + } +}