diff --git a/Lemegeton/Core/State.cs b/Lemegeton/Core/State.cs index 55786ba..f85c173 100644 --- a/Lemegeton/Core/State.cs +++ b/Lemegeton/Core/State.cs @@ -37,6 +37,7 @@ using Dalamud.Interface.Utility; using FFXIVClientStructs.FFXIV.Client.Game.Character; using Lemegeton.PacketHeaders; +using System.Text.RegularExpressions; namespace Lemegeton.Core { @@ -211,6 +212,7 @@ internal class MarkerApplication private PostCommandDelegate _postCmdFuncptr = null; public Dictionary SoftMarkers = new Dictionary(); internal Dictionary AllTimelines = new Dictionary(); + internal Dictionary TimelineOverrides = new Dictionary(); internal List MarkerHistory = new List(); @@ -413,27 +415,46 @@ public void PrepareFolder(string path) Directory.CreateDirectory(Path.GetDirectoryName(path)); } - public void LoadLocalTimelines() + public void LoadLocalTimelines(ushort territory) { try { PrepareFolder(cfg.TimelineLocalFolder); var timelinefiles = Directory.GetFiles(cfg.TimelineLocalFolder, "*.timeline.xml").OrderBy(x => new FileInfo(x).LastWriteTime); + Regex rex = new Regex("Lemegeton_(?[0-9]{1,})[^0-9]"); Dictionary tls = new Dictionary(); foreach (string fn in timelinefiles) { + Match m = rex.Match(fn); + if (m.Success == false) + { + continue; + } + ushort t = ushort.Parse(m.Groups["territory"].Value); + if (tls.ContainsKey(t) == true || (territory != 0 && t != territory)) + { + continue; + } Timeline tlx = LoadTimeline(fn); if (tlx != null) { tls[tlx.Territory] = tlx; } } - foreach (KeyValuePair tl in tls) + foreach (KeyValuePair kp in tls) { - Log(LogLevelEnum.Debug, null, "Timeline from {0} set to territory {1}", tl.Value.Filename, tl.Key); + lock (TimelineOverrides) + { + if (TimelineOverrides.ContainsKey(kp.Key)) + { + Log(LogLevelEnum.Debug, null, "Timeline from {0} not set to territory {1}, manually overridden", kp.Value.Filename, kp.Key); + continue; + } + } + Log(LogLevelEnum.Debug, null, "Timeline from {0} automatically set to territory {1}", kp.Value.Filename, kp.Key); lock (AllTimelines) { - AllTimelines[tl.Key] = tl.Value; + AllTimelines[kp.Key] = kp.Value; } } } @@ -442,7 +463,25 @@ public void LoadLocalTimelines() Log(LogLevelEnum.Error, ex, "Couldn't load timelines due to an exception"); } } - + + public void LoadOverriddenTimelines() + { + Dictionary tlcopy; + lock (TimelineOverrides) + { + tlcopy = new Dictionary(TimelineOverrides); + } + foreach (KeyValuePair kp in tlcopy) + { + Timeline tlx = LoadTimeline(kp.Value); + Log(LogLevelEnum.Debug, null, "Timeline override from {0} set to territory {1}", kp.Value, kp.Key); + lock (AllTimelines) + { + AllTimelines[kp.Key] = tlx; + } + } + } + public Timeline LoadTimeline(string filename) { try @@ -546,10 +585,12 @@ public void Initialize() cs.TerritoryChanged += Cs_TerritoryChanged; pi.UiBuilder.OpenMainUi += UiBuilder_OpenConfigUi; pi.UiBuilder.OpenConfigUi += UiBuilder_OpenConfigUi; + plug.DeserializeTimelineOverrides(cfg.PropertyBlob); if (cfg.TimelineLocalAllowed == true) { - LoadLocalTimelines(); + LoadLocalTimelines(0); } + LoadOverriddenTimelines(); Cs_TerritoryChanged(cs.TerritoryType); } @@ -656,7 +697,7 @@ internal void AutoselectTimeline(ushort territory) ClearReactionQueue(); } - private void Cs_TerritoryChanged(ushort e) + internal void Cs_TerritoryChanged(ushort e) { _timeline = null; AutoselectTimeline(e); diff --git a/Lemegeton/Core/UserInterface.cs b/Lemegeton/Core/UserInterface.cs index d9bd96b..7903c48 100644 --- a/Lemegeton/Core/UserInterface.cs +++ b/Lemegeton/Core/UserInterface.cs @@ -1,4 +1,5 @@ using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Internal; using ImGuiNET; using ImGuiScene; @@ -20,6 +21,8 @@ internal class UserInterface private Dictionary _jobs = new Dictionary(); private Dictionary _onDemand = new Dictionary(); + internal FileDialogManager _dialogManager = new FileDialogManager(); + internal State _state; private DateTime _loaded = DateTime.Now; @@ -57,7 +60,7 @@ internal enum MiscIconEnum } internal void LoadTextures() - { + { _misc[MiscIconEnum.Lemegeton] = GetTexture(33237); _misc[MiscIconEnum.BlueDiamond] = GetTexture(63937); _misc[MiscIconEnum.PurpleDiamond] = GetTexture(63939); @@ -354,6 +357,21 @@ internal ulong RenderJobSelector(ulong bitmap, bool allowLimited) return bitmap; } + internal static bool IconText(FontAwesomeIcon icon, string tooltip) + { + ImGui.PushFont(UiBuilder.IconFont); + string ico = icon.ToIconString(); + ImGui.Text(ico); + ImGui.PopFont(); + if (tooltip != null && ImGui.IsItemHovered() == true) + { + ImGui.BeginTooltip(); + ImGui.Text(tooltip); + ImGui.EndTooltip(); + } + return false; + } + internal static bool IconButton(FontAwesomeIcon icon, string tooltip) { ImGui.PushFont(UiBuilder.IconFont); @@ -364,10 +382,18 @@ internal static bool IconButton(FontAwesomeIcon icon, string tooltip) return true; } ImGui.PopFont(); - if (ImGui.IsItemHovered() == true && ImGui.IsItemActive() == false) + if (tooltip != null && ImGui.IsItemHovered() == true && ImGui.IsItemActive() == false) { ImGui.BeginTooltip(); - ImGui.Text(tooltip); + int cut = tooltip.IndexOf("##"); + if (cut >= 0) + { + ImGui.Text(tooltip.Substring(0, cut)); + } + else + { + ImGui.Text(tooltip); + } ImGui.EndTooltip(); } return false; @@ -640,6 +666,17 @@ internal void RenderWarning(string text) ImGui.SetCursorPos(new Vector2(tenp.X, Math.Max(anp1.Y, anp2.Y))); } + internal void ClearDialog() + { + _dialogManager.Reset(); + } + + internal void OpenFileDialog(string title, string filter, string startPath, Action> callback) + { + ClearDialog(); + _dialogManager.OpenFileDialog(title, filter, callback, 1, startPath, true); + } + } } diff --git a/Lemegeton/Language/English.cs b/Lemegeton/Language/English.cs index c7778f6..5c4e2b5 100644 --- a/Lemegeton/Language/English.cs +++ b/Lemegeton/Language/English.cs @@ -14,6 +14,21 @@ internal class English : Core.Language public English(State st) : base(st) { + #region 1.0.3.2 + AddEntry("Timelines/TimelineSelector", "Open timeline selector"); + AddEntry("Timelines/TimelineSelector/WindowTitle", "Lemegeton timeline selector"); + AddEntry("Timelines/TimelineSelector/SelectionInfo", "By default, Lemegeton will select the most recently edited timeline file it can find for a specific zone. You can add zone-specific overrides to make it always use the same timeline file. Please note that timeline reactions are generally tied to a specific timeline file."); + AddEntry("Timelines/TimelineSelector/ColZoneID", "ID"); + AddEntry("Timelines/TimelineSelector/ColZoneName", "Zone name"); + AddEntry("Timelines/TimelineSelector/ColZoneFile", "Timeline file in use"); + AddEntry("Timelines/TimelineSelector/ChangeTimelineFile", "Change timeline file"); + AddEntry("Timelines/TimelineSelector/DeleteOverride", "Remove timeline override"); + AddEntry("Timelines/TimelineSelector/SelTypeAutomatic", "Selected automatically"); + AddEntry("Timelines/TimelineSelector/SelTypeOverride", "Overridden manually"); + AddEntry("Timelines/TimelineSelector/SelectTimelineFile", "Select timeline file to use for '{0}'"); + AddEntry("Timelines/TimelineSelector/SelectTimelineFileUnspecified", "Select timeline file to add"); + AddEntry("Timelines/TimelineSelector/AddTimelineFile", "Add timeline file"); + #endregion #region 1.0.3.1 AddEntry("Content/Ultimate/UltDragonsongReprise/DoubleDragons", "(P6) Dragon HP difference indicator"); AddEntry("Content/Ultimate/UltDragonsongReprise/DoubleDragons/Enabled", "Enabled"); diff --git a/Lemegeton/Plugin.cs b/Lemegeton/Plugin.cs index ff15b04..a1afcbc 100644 --- a/Lemegeton/Plugin.cs +++ b/Lemegeton/Plugin.cs @@ -42,6 +42,8 @@ using System.Xml.Linq; using Dalamud.Plugin.Services; using Dalamud.Interface.Internal; +using static Dalamud.Interface.Utility.Raii.ImRaii; +using static FFXIVClientStructs.FFXIV.Common.Component.BGCollision.MeshPCB; namespace Lemegeton { @@ -54,7 +56,7 @@ public sealed class Plugin : IDalamudPlugin #else public string Name => "Lemegeton"; #endif - public string Version = "1.0.3.1"; + public string Version = "1.0.3.2"; internal class Downloadable { @@ -93,12 +95,15 @@ internal class ActionTypeItem private DateTime _loaded = DateTime.Now; private bool _aboutProg = false; private bool _softMarkerPreview = false; + private bool _timelineSelectorOpened = false; private DateTime _aboutOpened; private Dictionary _delDebugInput = new Dictionary(); private Queue _downloadQueue = new Queue(); private bool _downloadPending = false; private string _downloadFilename = ""; private string _timelineActionFilter = ""; + private string _timelineSelectorZoneFilter = ""; + private string _timelineSelectorFileFilter = ""; private bool _newNotifications = false; private int _ttsCounter = 1; @@ -295,7 +300,7 @@ public Plugin( ApplyVersionChanges(); I18n.OnFontDownload += I18n_OnFontDownload; InitializeLanguage(); - InitializeContent(); + InitializeContent(); _state.Initialize(); ApplyConfigToContent(); ChangeLanguage(_state.cfg.Language); @@ -413,6 +418,7 @@ private void Cs_Login() if (_drawingCallback == false) { _state.pi.UiBuilder.Draw += DrawUI; + _state.pi.UiBuilder.Draw += _ui._dialogManager.Draw; _drawingCallback = true; } } @@ -424,6 +430,7 @@ private void Cs_Logout() { if (_drawingCallback == true) { + _state.pi.UiBuilder.Draw -= _ui._dialogManager.Draw; _state.pi.UiBuilder.Draw -= DrawUI; _drawingCallback = false; } @@ -792,6 +799,25 @@ private void DeserializeContentCategory(XmlNode root, IEnumerable kp in _state.TimelineOverrides) + { + XmlNode cc = doc.CreateElement("Override"); + c.AppendChild(cc); + XmlAttribute a = doc.CreateAttribute("Territory"); + a.Value = kp.Key.ToString(); + cc.Attributes.Append(a); + a = doc.CreateAttribute("Filename"); + a.Value = kp.Value; + cc.Attributes.Append(a); + } + } + } return doc.OuterXml; } @@ -1936,6 +1980,11 @@ private void RenderTimelineContentHeader() ImGui.EndDisabled(); } ImGui.SameLine(); + if (UserInterface.IconButton(FontAwesomeIcon.Edit, I18n.Translate("Timelines/TimelineSelector")) == true) + { + OpenTimelineSelector(); + } + ImGui.SameLine(); ImGui.Text(I18n.Translate("Timelines/Profile")); ImGui.SameLine(); bool hasprofiles = tl != null && tl.Profiles.Count > 0; @@ -2085,6 +2134,12 @@ private void RenderTimelineContentHeader() ImGui.SetCursorPosY(ImGui.GetCursorPosY() + style.ItemSpacing.Y); } + private void OpenTimelineSelector() + { + Log(LogLevelEnum.Debug, "Opening timeline selector"); + _timelineSelectorOpened = true; + } + private float RenderTimelineContentEvents() { float start = ImGui.GetCursorPosY(); @@ -2385,7 +2440,7 @@ private float RenderTimelineReactionsList(out float ystart) { ImGui.BeginDisabled(); } - if (UserInterface.IconButton(FontAwesomeIcon.Plus, I18n.Translate("Timelines/NewReaction")) == true) + if (UserInterface.IconButton(FontAwesomeIcon.Plus, I18n.Translate("Timelines/Reactions/NewReaction")) == true) { string prname = _newReactionName.Trim(); Log(LogLevelEnum.Debug, "Creating new blank reaction as {0}", prname); @@ -2415,7 +2470,7 @@ private float RenderTimelineReactionsList(out float ystart) { ImGui.BeginDisabled(); } - if (UserInterface.IconButton(FontAwesomeIcon.Copy, I18n.Translate("Timelines/CloneReaction")) == true) + if (UserInterface.IconButton(FontAwesomeIcon.Copy, I18n.Translate("Timelines/Reactions/CloneReaction")) == true) { Timeline.Reaction orp = SelectedReaction; string orname = orp.Name; @@ -4250,7 +4305,7 @@ private void RenderSettingsTab() if (ImGui.Button(I18n.Translate("MainMenu/Settings/TimelineLocalReload")) == true) { Log(LogLevelEnum.Debug, "Reloading all local timelines"); - _state.LoadLocalTimelines(); + _state.LoadLocalTimelines(0); } if (ImGui.CollapsingHeader(I18n.Translate("MainMenu/Settings/TimelineOverlaySettings")) == true) { @@ -4648,6 +4703,7 @@ private void DrawMainWindow() SaveConfig(); ImGui.End(); ImGui.PopStyleColor(3); + _ui.ClearDialog(); return; } Vector2 c1 = ImGui.GetWindowPos(); @@ -4751,9 +4807,269 @@ private void DrawMainWindow() RenderFooter(); UserInterface.KeepWindowInSight(); ImGui.End(); + if (_timelineSelectorOpened == true) + { + DrawTimelineSelectorWindow(); + } ImGui.PopStyleColor(3); } + private void DrawTimelineSelectorWindow() + { + bool open = true; + ImGui.SetNextWindowSize(new Vector2(500, 500), ImGuiCond.FirstUseEver); + if (ImGui.Begin(I18n.Translate("Timelines/TimelineSelector/WindowTitle"), ref open, ImGuiWindowFlags.NoCollapse) == false) + { + ImGui.End(); + return; + } + if (open == false) + { + Log(LogLevelEnum.Debug, "Closing timeline selector"); + _timelineSelectorOpened = false; + ImGui.End(); + _ui.ClearDialog(); + return; + } + _ui.RenderWarning(I18n.Translate("Timelines/TimelineSelector/SelectionInfo")); + ImGui.BeginTable("Lemegeton_TimelineSelector", 4, ImGuiTableFlags.Resizable | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY); + ImGui.TableSetupScrollFreeze(0, 2); + ImGui.TableSetupColumn(I18n.Translate("Timelines/TimelineSelector/ColZoneID"), ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoSort); + ImGui.TableSetupColumn(I18n.Translate("Timelines/TimelineSelector/ColZoneName"), ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoSort); + ImGui.TableSetupColumn(I18n.Translate("Timelines/TimelineSelector/ColZoneFile"), ImGuiTableColumnFlags.WidthStretch | ImGuiTableColumnFlags.NoSort); + ImGui.TableSetupColumn("##Lemegeton_TimelineSelector_actions", ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoSort | ImGuiTableColumnFlags.NoResize); + ImGui.TableHeadersRow(); + Dictionary tlcopy; + lock (_state.AllTimelines) + { + tlcopy = new Dictionary(_state.AllTimelines); + } + Dictionary zonenames = new Dictionary(); + foreach (ushort t in tlcopy.Keys) + { + zonenames[t] = GetInstanceNameForTerritory(t); + } + var temp = zonenames.ToList(); + temp.Sort((a, b) => a.Value.CompareTo(b.Value)); + ImGuiStylePtr style = ImGui.GetStyle(); + float itemsp = style.ItemSpacing.X; + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + Vector2 avail3 = ImGui.GetContentRegionAvail(); + ImGui.Text(" "); + ImGui.TableSetColumnIndex(1); + Vector2 avail1 = ImGui.GetContentRegionAvail(); + ImGui.PushFont(UiBuilder.IconFont); + string ico = FontAwesomeIcon.Search.ToIconString(); + Vector2 sz = ImGui.CalcTextSize(ico); + ImGui.Text(ico); + ImGui.PopFont(); + ImGui.SameLine(); + ImGui.PushItemWidth(avail1.X - sz.X - itemsp); + if (ImGui.InputText("##TimelineSelector/ZoneFilterBox", ref _timelineSelectorZoneFilter, 256) == true) + { + } + ImGui.PopItemWidth(); + ImGui.TableSetColumnIndex(2); + Vector2 avail2 = ImGui.GetContentRegionAvail(); + ImGui.PushFont(UiBuilder.IconFont); + ImGui.Text(ico); + ImGui.PopFont(); + ImGui.SameLine(); + ImGui.PushItemWidth(avail2.X - sz.X - itemsp); + if (ImGui.InputText("##TimelineSelector/FileFilterBox", ref _timelineSelectorFileFilter, 256) == true) + { + } + ImGui.PopItemWidth(); + ImGui.TableSetColumnIndex(3); + ImGui.Text(" "); + temp = (from tx in temp where + ( + _timelineSelectorZoneFilter == "" + || + tx.Value.Contains(_timelineSelectorZoneFilter, StringComparison.InvariantCultureIgnoreCase) + || + tx.Key.ToString().Contains(_timelineSelectorZoneFilter, StringComparison.InvariantCultureIgnoreCase)) + && + (_timelineSelectorFileFilter == "" || tlcopy[tx.Key].Filename.Contains(_timelineSelectorFileFilter, StringComparison.InvariantCultureIgnoreCase)) + select tx).ToList(); + foreach (var zn in temp) + { + ImGui.TableNextRow(); + bool overridden; + lock (_state.TimelineOverrides) + { + overridden = _state.TimelineOverrides.ContainsKey(zn.Key); + } + for (int col = 0; col < 4; col++) + { + ImGui.TableSetColumnIndex(col); + switch (col) + { + case 0: + { + string txt = zn.Key.ToString(); + Vector2 psz = ImGui.CalcTextSize(txt); + ImGui.Text(txt); + if (psz.X > avail3.X && ImGui.IsItemHovered() == true) + { + ImGui.BeginTooltip(); + ImGui.Text(txt); + ImGui.EndTooltip(); + } + } + break; + case 1: + { + string txt = zn.Value; + Vector2 psz = ImGui.CalcTextSize(txt); + ImGui.Text(txt); + if (psz.X > avail1.X && ImGui.IsItemHovered() == true) + { + ImGui.BeginTooltip(); + ImGui.Text(txt); + ImGui.EndTooltip(); + } + } + break; + case 2: + { + if (overridden == false) + { + ImGui.PushStyleColor(ImGuiCol.Text, ImGui.GetColorU32(new Vector4(0.5f, 1.0f, 0.5f, 0.7f))); + UserInterface.IconText(FontAwesomeIcon.Sync, I18n.Translate("Timelines/TimelineSelector/SelTypeAutomatic")); + ImGui.PopStyleColor(); + } + else + { + ImGui.PushStyleColor(ImGuiCol.Text, ImGui.GetColorU32(new Vector4(1.0f, 0.5f, 0.5f, 0.7f))); + UserInterface.IconText(FontAwesomeIcon.Edit, I18n.Translate("Timelines/TimelineSelector/SelTypeOverride")); + ImGui.PopStyleColor(); + } + ImGui.SameLine(); + string txt = tlcopy[zn.Key].Filename; + Vector2 psz = ImGui.CalcTextSize(txt); + if (ImGui.Selectable(txt, false) == true) + { + System.Diagnostics.Process.Start("explorer.exe", String.Format("/select, \"{0}\"", txt)); + } + if (psz.X > avail2.X && ImGui.IsItemHovered() == true) + { + ImGui.BeginTooltip(); + ImGui.Text(txt); + ImGui.EndTooltip(); + } + } + break; + case 3: + { + if (UserInterface.IconButton(FontAwesomeIcon.FolderOpen, I18n.Translate("Timelines/TimelineSelector/ChangeTimelineFile") + "##" + zn.Key) == true) + { + string sp = Path.GetDirectoryName(tlcopy[zn.Key].Filename); + Log(LogLevelEnum.Debug, "Opening timeline file selection dialog to {0}", sp); + _ui.OpenFileDialog(I18n.Translate("Timelines/TimelineSelector/SelectTimelineFile", zn.Value), ".xml", sp, (ok, filename) => + { + if (ok == true) + { + SetTimelineOverrideToFile(zn.Key, filename.First()); + } + }); + } + if (overridden == true) + { + ImGui.SameLine(); + if (UserInterface.IconButton(FontAwesomeIcon.Trash, I18n.Translate("Timelines/TimelineSelector/DeleteOverride") + "##" + zn.Key) == true) + { + RemoveTimelineOverrideFrom(zn.Key); + } + } + } + break; + } + } + } + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + ImGui.Text(""); + ImGui.TableSetColumnIndex(1); + ImGui.Text(""); + ImGui.TableSetColumnIndex(2); + ImGui.Text(""); + ImGui.TableSetColumnIndex(3); + if (UserInterface.IconButton(FontAwesomeIcon.Plus, I18n.Translate("Timelines/TimelineSelector/AddTimelineFile")) == true) + { + string sp = _state.cfg.TimelineLocalFolder; + Log(LogLevelEnum.Debug, "Opening timeline file selection dialog to {0}", sp); + _ui.OpenFileDialog(I18n.Translate("Timelines/TimelineSelector/SelectTimelineFileUnspecified"), ".xml", sp, (ok, filename) => + { + if (ok == true) + { + string fn = filename.First(); + Timeline tl = _state.LoadTimeline(fn); + if (tl != null) + { + SetTimelineOverrideToFile(tl.Territory, fn); + } + } + }); + } + ImGui.EndTable(); + UserInterface.KeepWindowInSight(); + ImGui.End(); + } + + private void RemoveTimelineOverrideFrom(ushort territory) + { + Log(LogLevelEnum.Debug, "Removing timeline override from territory {0}", territory); + lock (_state.TimelineOverrides) + { + if (_state.TimelineOverrides.ContainsKey(territory) == false) + { + return; + } + _state.TimelineOverrides.Remove(territory); + } + lock (_state.AllTimelines) + { + if (_state.AllTimelines.ContainsKey(territory) == true) + { + _state.AllTimelines.Remove(territory); + } + } + if (_state.cfg.TimelineLocalAllowed == true) + { + _state.LoadLocalTimelines(territory); + } + } + + private void SetTimelineOverrideToFile(ushort territory, string filename) + { + Log(LogLevelEnum.Debug, "Setting timeline override for territory {0} to {1}", territory, filename); + Timeline tl = _state.LoadTimeline(filename); + if (tl == null) + { + Log(LogLevelEnum.Warning, "Timeline file {0} could not be loaded as override for territory {1}", filename, territory); + return; + } + if (tl.Territory != territory) + { + Log(LogLevelEnum.Warning, "Timeline file {0} is meant for another territory {1}, not territory {2}", filename, tl.Territory, territory); + return; + } + lock (_state.AllTimelines) + { + _state.AllTimelines[territory] = tl; + } + lock (_state.TimelineOverrides) + { + _state.TimelineOverrides[territory] = filename; + } + if (territory == _state.cs.TerritoryType) + { + _state.Cs_TerritoryChanged(territory); + } + } + private void RenderMethodCall(Delegate del) { Type delt = del.GetType();