From a77dbfe187b53cdc1e66dfe9de838d16047c20fc Mon Sep 17 00:00:00 2001 From: ditzy Date: Sat, 25 Nov 2023 09:45:20 -0600 Subject: [PATCH] add mode toggle and full text copy --- .gitmodules | 3 + OtterGui | 1 + ScoutHelper.sln | 12 ++ ScoutHelper.sln.DotSettings | 9 ++ ScoutHelper/Configuration.cs | 9 +- ScoutHelper/Constants.cs | 12 +- ScoutHelper/Localization/Strings.Designer.cs | 119 +++++++++++++++- ScoutHelper/Localization/Strings.resx | 53 +++++++- ScoutHelper/Managers/BearManager.cs | 52 +++---- ScoutHelper/Models/TrainMob.cs | 2 +- ScoutHelper/Plugin.cs | 6 +- ScoutHelper/ScoutHelper.csproj | 4 + ScoutHelper/Utils.cs | 85 ++++++++++-- ScoutHelper/Windows/ConfigWindow.cs | 80 ++++++++++- ScoutHelper/Windows/ImGuiPlus.cs | 133 ++++++++++++++++++ ScoutHelper/Windows/MainWindow.cs | 134 ++++++++++++++----- ScoutHelper/packages.lock.json | 9 ++ 17 files changed, 631 insertions(+), 92 deletions(-) create mode 100644 .gitmodules create mode 160000 OtterGui create mode 100644 ScoutHelper/Windows/ImGuiPlus.cs diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ec79cf1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "OtterGui"] + path = OtterGui + url = git@github.com:Ottermandias/OtterGui.git diff --git a/OtterGui b/OtterGui new file mode 160000 index 0000000..a64327f --- /dev/null +++ b/OtterGui @@ -0,0 +1 @@ +Subproject commit a64327f6c7db057d8b1cc1e5b11f392cf5947034 diff --git a/ScoutHelper.sln b/ScoutHelper.sln index 176646d..a95556d 100644 --- a/ScoutHelper.sln +++ b/ScoutHelper.sln @@ -7,6 +7,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScoutHelper", "ScoutHelper\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScoutHelperTests", "ScoutHelperTests\ScoutHelperTests.csproj", "{31CD702B-08BA-4EB3-93D6-C895CD3DCB19}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OtterGui", "OtterGui\OtterGui.csproj", "{264A8375-1760-4D5B-87D2-9C71AC2D6460}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OtterGuiInternal", "OtterGui\OtterGuiInternal\OtterGuiInternal.csproj", "{A14FFD18-747B-4FCC-AD14-5A51A28D4710}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -21,6 +25,14 @@ Global {31CD702B-08BA-4EB3-93D6-C895CD3DCB19}.Debug|x64.Build.0 = Debug|Any CPU {31CD702B-08BA-4EB3-93D6-C895CD3DCB19}.Release|x64.ActiveCfg = Release|Any CPU {31CD702B-08BA-4EB3-93D6-C895CD3DCB19}.Release|x64.Build.0 = Release|Any CPU + {264A8375-1760-4D5B-87D2-9C71AC2D6460}.Debug|x64.ActiveCfg = Debug|Any CPU + {264A8375-1760-4D5B-87D2-9C71AC2D6460}.Debug|x64.Build.0 = Debug|Any CPU + {264A8375-1760-4D5B-87D2-9C71AC2D6460}.Release|x64.ActiveCfg = Release|Any CPU + {264A8375-1760-4D5B-87D2-9C71AC2D6460}.Release|x64.Build.0 = Release|Any CPU + {A14FFD18-747B-4FCC-AD14-5A51A28D4710}.Debug|x64.ActiveCfg = Debug|Any CPU + {A14FFD18-747B-4FCC-AD14-5A51A28D4710}.Debug|x64.Build.0 = Debug|Any CPU + {A14FFD18-747B-4FCC-AD14-5A51A28D4710}.Release|x64.ActiveCfg = Release|Any CPU + {A14FFD18-747B-4FCC-AD14-5A51A28D4710}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ScoutHelper.sln.DotSettings b/ScoutHelper.sln.DotSettings index 4619e93..6266888 100644 --- a/ScoutHelper.sln.DotSettings +++ b/ScoutHelper.sln.DotSettings @@ -1,8 +1,17 @@  False False + 0 + 1 + 1 + False + False + False + True + CHOP_IF_LONG True True + True CHOP_IF_LONG True True diff --git a/ScoutHelper/Configuration.cs b/ScoutHelper/Configuration.cs index 53b49e3..dc5c0a2 100644 --- a/ScoutHelper/Configuration.cs +++ b/ScoutHelper/Configuration.cs @@ -9,7 +9,7 @@ public class Configuration : IPluginConfiguration { // the below exist just to make saving less cumbersome [NonSerialized] - private DalamudPluginInterface? _pluginInterface; + private DalamudPluginInterface _pluginInterface = null!; public int Version { get; set; } = 0; @@ -19,11 +19,14 @@ public class Configuration : IPluginConfiguration { public string BearSiteTrainUrl { get; set; } = "https://tracker.beartoolkit.com/train"; public string BearTrainName { get; set; } = "Scout Helper Train"; + public string CopyTemplate { get; set; } = Constants.DefaultCopyTemplate; + public bool IsCopyModeFullText { get; set; } = false; + public void Initialize(DalamudPluginInterface pluginInterface) { - this._pluginInterface = pluginInterface; + _pluginInterface = pluginInterface; } public void Save() { - _pluginInterface!.SavePluginConfig(this); + _pluginInterface.SavePluginConfig(this); } } diff --git a/ScoutHelper/Constants.cs b/ScoutHelper/Constants.cs index 4f9b908..240d863 100644 --- a/ScoutHelper/Constants.cs +++ b/ScoutHelper/Constants.cs @@ -4,20 +4,30 @@ namespace ScoutHelper; public static class Constants { - #region plugin constants + public const string PluginName = "Scout Helper"; public static readonly string PluginVersion; public static readonly string PluginNamespace = PluginName.Replace(" ", ""); + + #endregion + + #region core constants + + public const string DefaultCopyTemplate = "{patch} {#}/{#max} {world} [{tracker}]({link})"; + #endregion #region web constants + public static readonly MediaTypeWithQualityHeaderValue MediaTypeJson = MediaTypeWithQualityHeaderValue.Parse("application/json"); + public static readonly ProductInfoHeaderValue UserAgent = new ProductInfoHeaderValue( PluginNamespace, PluginVersion ); + #endregion static Constants() { diff --git a/ScoutHelper/Localization/Strings.Designer.cs b/ScoutHelper/Localization/Strings.Designer.cs index 3c02967..81b3a2b 100644 --- a/ScoutHelper/Localization/Strings.Designer.cs +++ b/ScoutHelper/Localization/Strings.Designer.cs @@ -78,16 +78,62 @@ internal static string BearButtonTooltip { } /// - /// Looks up a localized string similar to At this time there are no configurable settings for Scout Helper, but there will be in the future. Please use command `/scouth` to open the Scout Helper window.. + /// Looks up a localized string similar to DESCRIPTION:. /// - internal static string ConfigWindowContent { + internal static string ConfigWindowDescriptionLabel { get { - return ResourceManager.GetString("ConfigWindowContent", resourceCulture); + return ResourceManager.GetString("ConfigWindowDescriptionLabel", resourceCulture); } } /// - /// Looks up a localized string similar to Scout Helper Settings. + /// Looks up a localized string similar to PREVIEW:. + /// + internal static string ConfigWindowPreviewLabel { + get { + return ResourceManager.GetString("ConfigWindowPreviewLabel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to FULL-TEXT TEMPLATE. + /// + internal static string ConfigWindowSectionLabelFullText { + get { + return ResourceManager.GetString("ConfigWindowSectionLabelFullText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configure the template used by the full-text copy mode. When the full-text mode is selected, this template is what will be copied to your clipboard, rather than just the generated tracker link. Variables can be referenced by surrounding them with curly braces (e.g. "{link}"), and the following variables are available:. + /// + internal static string ConfigWindowTemplateDesc { + get { + return ResourceManager.GetString("ConfigWindowTemplateDesc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {#} - the number of hunt marks in the train. + /// + ///{#max} - the maximum possible number of hunt marks in the patch. + /// + ///{link} - the generated tracker link \(≧▽≦)/ + /// + ///{patch} - the latest patch contained in the recorded train. If there are 6 HW marks and 1 SHB mark in the train, the patch will be SHB. + /// + ///{tracker} - the name of the tracker used (e.g. "bear"). + /// + ///{world} - the current world you are in. Make sure to use the link generator before you change worlds, if you include this in your template ;P. + /// + internal static string ConfigWindowTemplateVariables { + get { + return ResourceManager.GetString("ConfigWindowTemplateVariables", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SCOUT HELPER SETTINGS. /// internal static string ConfigWindowTitle { get { @@ -96,7 +142,70 @@ internal static string ConfigWindowTitle { } /// - /// Looks up a localized string similar to Scout Helper. + /// Looks up a localized string similar to full-text. + /// + internal static string CopyModeFullTextButton { + get { + return ResourceManager.GetString("CopyModeFullTextButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to link. + /// + internal static string CopyModeLinkButton { + get { + return ResourceManager.GetString("CopyModeLinkButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to copies a full-text template to your clipboard, which can contain data about the scouted marks beyond just the tracker link. click on the '?' currently underneath your mouse, to open the SCOUT HELPER settings and configure the template, or open the settings from the Dalamud plugin installer screen.. + /// + internal static string CopyModeTooltipFullTextDesc { + get { + return ResourceManager.GetString("CopyModeTooltipFullTextDesc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to only copies the generated tracker link to your clipboard. + /// + internal static string CopyModeTooltipLinkDesc { + get { + return ResourceManager.GetString("CopyModeTooltipLinkDesc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The copy mode that tracker generators will use when generating tracker links.. + /// + internal static string CopyModeTooltipSummary { + get { + return ResourceManager.GetString("CopyModeTooltipSummary", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GENERATORS. + /// + internal static string MainWindowSectionLabelGenerators { + get { + return ResourceManager.GetString("MainWindowSectionLabelGenerators", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MODE. + /// + internal static string MainWindowSectionLabelMode { + get { + return ResourceManager.GetString("MainWindowSectionLabelMode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SCOUT HELPER. /// internal static string MainWindowTitle { get { diff --git a/ScoutHelper/Localization/Strings.resx b/ScoutHelper/Localization/Strings.resx index 104db05..0075eed 100644 --- a/ScoutHelper/Localization/Strings.resx +++ b/ScoutHelper/Localization/Strings.resx @@ -22,13 +22,10 @@ Bear Toolkit - Scout Helper + SCOUT HELPER - Scout Helper Settings - - - At this time there are no configurable settings for Scout Helper, but there will be in the future. Please use command `/scouth` to open the Scout Helper window. + SCOUT HELPER SETTINGS Generate a Bear Toolkit link from the Hunt Helper train recorder. @@ -39,4 +36,50 @@ Siren Hunts is not yet implemented. Coming soon! + + link + + + full-text + + + MODE + + + GENERATORS + + + FULL-TEXT TEMPLATE + + + The copy mode that tracker generators will use when generating tracker links. + + + only copies the generated tracker link to your clipboard + + + copies a full-text template to your clipboard, which can contain data about the scouted marks beyond just the tracker link. click on the '?' currently underneath your mouse, to open the SCOUT HELPER settings and configure the template, or open the settings from the Dalamud plugin installer screen. + + + PREVIEW: + + + Configure the template used by the full-text copy mode. When the full-text mode is selected, this template is what will be copied to your clipboard, rather than just the generated tracker link. Variables can be referenced by surrounding them with curly braces (e.g. "{link}"), and the following variables are available: + + + {#} - the number of hunt marks in the train. + +{#max} - the maximum possible number of hunt marks in the patch. + +{link} - the generated tracker link \(≧▽≦)/ + +{patch} - the latest patch contained in the recorded train. If there are 6 HW marks and 1 SHB mark in the train, the patch will be SHB. + +{tracker} - the name of the tracker used (e.g. "bear"). + +{world} - the current world you are in. Make sure to use the link generator before you change worlds, if you include this in your template ;P + + + DESCRIPTION: + diff --git a/ScoutHelper/Managers/BearManager.cs b/ScoutHelper/Managers/BearManager.cs index 443d0f1..4cddeec 100644 --- a/ScoutHelper/Managers/BearManager.cs +++ b/ScoutHelper/Managers/BearManager.cs @@ -14,7 +14,6 @@ namespace ScoutHelper.Managers; public class BearManager : IDisposable { - private static HttpClient HttpClient { get; } = new(); private IDictionary MobIdToBearName { get; init; } @@ -34,7 +33,6 @@ public void Dispose() { } private static IDictionary LoadData(string dataFilePath) { - if (!File.Exists(dataFilePath)) { throw new Exception($"Can't find {dataFilePath}"); } @@ -50,6 +48,7 @@ public void Dispose() { if (!Enum.TryParse(patchData.Key, out Patch patch)) { throw new Exception($"Unknown patch: {patchData.Key}"); } + return (patchData.Value as IDictionary).Select( mob => { var mobName = mob.Key; @@ -70,6 +69,7 @@ private BearApiSpawnPoint CreateRequestSpawnPoint(TrainMob mob) { if (mob.Instance is >= 1 and <= 9) { huntName += $" {mob.Instance}"; } + return new BearApiSpawnPoint( huntName, mob.Position.X, @@ -78,20 +78,22 @@ private BearApiSpawnPoint CreateRequestSpawnPoint(TrainMob mob) { ); } - public async Task> GenerateBearLink( + public async Task> GenerateBearLink( string worldName, - IList trainMobs + IEnumerable trainMobs ) { var bearSupportedMobs = trainMobs.Where(mob => MobIdToBearName.ContainsKey(mob.MobId)).ToList(); + if (bearSupportedMobs.Count == 0) + return "No mobs supported by Bear Toolkit were found in the Hunt Helper train recorder ;-;"; + var spawnPoints = bearSupportedMobs.Select(CreateRequestSpawnPoint).ToList(); - var patchName = bearSupportedMobs + var highestPatch = bearSupportedMobs .Select(mob => MobIdToBearName[mob.MobId].patch) .Distinct() - .Max() - .BearName(); + .Max(); var requestPayload = JsonConvert.SerializeObject( - new BearApiTrainRequest(worldName, Plugin.Conf.BearTrainName, patchName, spawnPoints) + new BearApiTrainRequest(worldName, Plugin.Conf.BearTrainName, highestPatch.BearName(), spawnPoints) ); Plugin.Log.Debug("Request payload: {0}", requestPayload); var requestContent = new StringContent(requestPayload, Encoding.UTF8, Constants.MediaTypeJson); @@ -110,23 +112,19 @@ IList trainMobs var trainInfo = JsonConvert.DeserializeObject(responseJson).Trains.First(); var url = $"{Plugin.Conf.BearSiteTrainUrl}/{trainInfo.TrainId}"; - return (url, trainInfo.Password); - } - catch (TimeoutException) { + return new BearLinkData(url, trainInfo.Password, highestPatch); + } catch (TimeoutException) { const string message = "Timed out posting the train to Bear ;-;"; Plugin.Log.Error(message); return message; - } - catch (OperationCanceledException e) { + } catch (OperationCanceledException e) { const string message = "Generating the Bear link was canceled >_>"; Plugin.Log.Warning(e, message); return message; - } - catch (HttpRequestException e) { + } catch (HttpRequestException e) { Plugin.Log.Error(e, "Posting the train to Bear failed."); return "Something failed when communicating with Bear :T"; - } - catch (Exception e) { + } catch (Exception e) { const string message = "An unknown error happened while generating the Bear link D:"; Plugin.Log.Error(e, message); return message; @@ -134,15 +132,21 @@ IList trainMobs } } +public record struct BearLinkData( + string Url, + string Password, + Patch HighestPatch +) { } + public static class BearExtensions { - private static readonly IDictionary BearPatchNames = new Dictionary { - {Patch.ARR, "ARR"}, - {Patch.HW, "HW"}, - {Patch.SB, "SB"}, - {Patch.SHB, "ShB"}, - {Patch.EW, "EW"} + private static readonly IReadOnlyDictionary BearPatchNames = new Dictionary { + { Patch.ARR, "ARR" }, + { Patch.HW, "HW" }, + { Patch.SB, "SB" }, + { Patch.SHB, "ShB" }, + { Patch.EW, "EW" } }.VerifyEnumDictionary(); - + public static string BearName(this Patch patch) { return BearPatchNames[patch]; } diff --git a/ScoutHelper/Models/TrainMob.cs b/ScoutHelper/Models/TrainMob.cs index 10bedd2..4b4f125 100644 --- a/ScoutHelper/Models/TrainMob.cs +++ b/ScoutHelper/Models/TrainMob.cs @@ -8,7 +8,7 @@ public record struct TrainMob( uint MobId, uint TerritoryId, uint MapId, - uint Instance, + uint? Instance, Vector2 Position, bool Dead, DateTime LastSeenUtc diff --git a/ScoutHelper/Plugin.cs b/ScoutHelper/Plugin.cs index 3c70bf1..deebb94 100644 --- a/ScoutHelper/Plugin.cs +++ b/ScoutHelper/Plugin.cs @@ -44,10 +44,10 @@ public Plugin() { Conf.Initialize(PluginInterface); HuntHelperManager = new HuntHelperManager(); - BearManager = new BearManager(Utils.DataFilePath("Bear.json")); + BearManager = new BearManager(Utils.PluginFilePath(@"Data\Bear.json")); ConfigWindow = new ConfigWindow(); - MainWindow = new MainWindow(HuntHelperManager, BearManager); + MainWindow = new MainWindow(HuntHelperManager, BearManager, ConfigWindow); PluginInterface.LanguageChanged += OnLanguageChanged; OnLanguageChanged(PluginInterface.UiLanguage); @@ -73,6 +73,8 @@ public void Dispose() { MainWindow.Dispose(); HuntHelperManager.Dispose(); + + Conf.Save(); } private static void OnLanguageChanged(string languageCode) { diff --git a/ScoutHelper/ScoutHelper.csproj b/ScoutHelper/ScoutHelper.csproj index 23431ad..7c00086 100644 --- a/ScoutHelper/ScoutHelper.csproj +++ b/ScoutHelper/ScoutHelper.csproj @@ -36,4 +36,8 @@ PreserveNewest + + + + diff --git a/ScoutHelper/Utils.cs b/ScoutHelper/Utils.cs index 1570c0d..39f01fa 100644 --- a/ScoutHelper/Utils.cs +++ b/ScoutHelper/Utils.cs @@ -1,24 +1,26 @@ -using Dalamud.Plugin.Services; -using ImGuiNET; -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; +using System.Numerics; +using System.Text; +using System.Text.RegularExpressions; +using Dalamud.Plugin.Services; +using ImGuiNET; +using ScoutHelper.Models; namespace ScoutHelper; public static class Utils { - public static string WorldName { - get => Plugin.ClientState.LocalPlayer?.CurrentWorld?.GameData?.Name.ToString() ?? "Not Found"; - } + public static string WorldName => + Plugin.ClientState.LocalPlayer?.CurrentWorld?.GameData?.Name.ToString() ?? "Not Found"; - public static string DataFilePath(string dataFilename) => Path.Combine( + public static string PluginFilePath(string dataFilename) => Path.Combine( Plugin.PluginInterface.AssemblyLocation.Directory?.FullName!, - "Data", dataFilename ); - + public static void CreateTooltip(string text, float width = 12f) { ImGui.BeginTooltip(); ImGui.PushTextWrapPos(ImGui.GetFontSize() * width); @@ -27,7 +29,55 @@ public static void CreateTooltip(string text, float width = 12f) { ImGui.EndTooltip(); } + public static Vector2 V2(float x, float y) => new(x, y); + + public static string FormatTemplate( + string textTemplate, + IList trainList, + string tracker, + Patch highestPatch, + string url + ) { + var regex = new Regex(@"\\?\{[^{}]+\\?\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); + var matches = regex.Matches(textTemplate); + + if (matches.Count == 0) { + return textTemplate; + } + + var variables = new Dictionary() { + { "#", trainList.Count.ToString() }, + { "#max", trainList.Count.ToString() }, + { "link", url }, + { "patch", highestPatch.ToString() }, + { "tracker", tracker }, + { "world", WorldName }, + }.AsReadOnly(); + + var s = new StringBuilder(textTemplate.Length); + var i = 0; + foreach (Match match in matches) { + var m = match.Value; + if (m[0] == '\\' || m[^2] == '\\') continue; + + var variable = m.Substring(1, m.Length - 2).ToLowerInvariant(); + if (!variables.ContainsKey(variable)) continue; + + s.Append(textTemplate.AsSpan(i, match.Index - i)); + s.Append(variables[variable]); + + i = match.Index + match.Length; + } + + if (i < textTemplate.Length) { + s.Append(textTemplate.AsSpan(i)); + } + + return s.ToString(); + } + #region extensions + public static void TaggedPrint(this IChatGui chatGui, string message) { chatGui.Print(message, Plugin.Name); } @@ -36,12 +86,27 @@ public static void TaggedPrintError(this IChatGui chatGui, string message) { chatGui.PrintError(message, Plugin.Name); } - public static IDictionary VerifyEnumDictionary(this IDictionary enumDict) where K : struct, Enum { + public static IReadOnlyDictionary VerifyEnumDictionary(this IDictionary enumDict) + where K : struct, Enum { var allEnumsAreInDict = (Enum.GetValuesAsUnderlyingType() as K[])!.All(enumDict.ContainsKey); if (!allEnumsAreInDict) { throw new Exception($"All values of enum [{typeof(K).Name}] must be in the dictionary."); } + return enumDict.ToImmutableDictionary(); } + + public static IEnumerable ForEach(this IEnumerable source, Action action) => + source.ForEach((_, value) => action.Invoke(value)); + + public static IEnumerable ForEach(this IEnumerable source, Action action) { + var values = source as T[] ?? source.ToArray(); + for (var i = 0U; i < values.Length; ++i) { + action.Invoke(i, values[i]); + } + + return values; + } + #endregion } diff --git a/ScoutHelper/Windows/ConfigWindow.cs b/ScoutHelper/Windows/ConfigWindow.cs index e1cd152..d7f6bfe 100644 --- a/ScoutHelper/Windows/ConfigWindow.cs +++ b/ScoutHelper/Windows/ConfigWindow.cs @@ -3,25 +3,93 @@ using ImGuiNET; using ScoutHelper.Localization; using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; using System.Numerics; +using System.Text; +using Dalamud.Interface; +using Dalamud.Memory; +using OtterGui.Raii; +using ScoutHelper.Models; +using static ScoutHelper.Utils; namespace ScoutHelper.Windows; public class ConfigWindow : Window, IDisposable { + private string _fullTextTemplate = Plugin.Conf.CopyTemplate; + private string _previewFullText; + + private static readonly IList PreviewTrainList = new[] { + "Gourmand", "Chef's Kiss", "Little Mischief", "Poub" + } + .Select( + name => new TrainMob(name, 3654, 1634, 3214, 2, V2(35, 64), false, DateTime.Now) + ) + .ToImmutableList(); public ConfigWindow() : base( - Strings.ConfigWindowTitle, - ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar + Strings.ConfigWindowTitle ) { SizeConstraints = new WindowSizeConstraints() { - MinimumSize = new Vector2(128 * ImGuiHelpers.GlobalScale, 0), - MaximumSize = new Vector2(float.MaxValue, float.MaxValue) + MinimumSize = V2(384, 256), + MaximumSize = V2(float.MaxValue, float.MaxValue) }; + + _previewFullText = ComputePreviewFullText(); } - public void Dispose() { } + public void Dispose() { + UpdateConfig(); + + GC.SuppressFinalize(this); + } + + private void UpdateConfig() { + Plugin.Conf.CopyTemplate = _fullTextTemplate; + Plugin.Conf.Save(); + + Plugin.Log.Debug("config saved"); + } public override void Draw() { - ImGui.TextWrapped(Strings.ConfigWindowContent); + ImGuiPlus.Heading(Strings.ConfigWindowSectionLabelFullText); + DrawParagraphSpacing(); + + var textWasEdited = ImGui.InputTextMultiline( + string.Empty, + ref _fullTextTemplate, + 255, + ImGui.GetContentRegionAvail() with { Y = ImGui.GetFontSize() * 4.75f } + ); + if (ImGui.IsItemDeactivatedAfterEdit()) UpdateConfig(); + if (textWasEdited) _previewFullText = ComputePreviewFullText(); + + ImGui.Text(Strings.ConfigWindowPreviewLabel); + ImGui.Indent(); + ImGui.TextDisabled(_previewFullText); + ImGui.Unindent(); + + ImGui.NewLine(); + + ImGui.Text(Strings.ConfigWindowDescriptionLabel); + DrawParagraphSpacing(); + ImGui.TextWrapped(Strings.ConfigWindowTemplateDesc); + DrawParagraphSpacing(); + ImGui.Indent(); + ImGui.TextWrapped(Strings.ConfigWindowTemplateVariables); + ImGui.Unindent(); } + + private static void DrawParagraphSpacing() { + ImGui.Dummy(V2(0, 0.50f * ImGui.GetFontSize())); + } + + private string ComputePreviewFullText() => FormatTemplate( + _fullTextTemplate, + PreviewTrainList, + "bear", + Patch.SHB, + "https://example.com" + ); } diff --git a/ScoutHelper/Windows/ImGuiPlus.cs b/ScoutHelper/Windows/ImGuiPlus.cs new file mode 100644 index 0000000..681029c --- /dev/null +++ b/ScoutHelper/Windows/ImGuiPlus.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using static OtterGui.Widgets.ToggleButton; + +namespace ScoutHelper.Windows; + +public static class ImGuiPlus { + private static readonly Stack CursorPosStack = new(4); + + public static Vector2 WithCursorPos(Action action) => + WithCursorPos( + cursorPos => { + action.Invoke(cursorPos); + return null; + } + ).finalCursorPos; + + public static (Vector2 finalCursorPos, T result) WithCursorPos(Func function) => + WithCursorPos(_ => function.Invoke()); + + public static (Vector2 finalCursorPos, T result) WithCursorPos(Func function) { + var startingCursorPos = PushCursorPos(); + Vector2 finalCursorPos; + T result; + try { + result = function.Invoke(startingCursorPos); + } finally { + finalCursorPos = PopCursorPos(); + } + return (finalCursorPos, result); + } + + public static Vector2 PushCursorPos() { + var cursorPos = ImGui.GetCursorPos(); + CursorPosStack.Push(cursorPos); + return cursorPos; + } + + public static Vector2 PopCursorPos() { + var finalCursorPos = ImGui.GetCursorPos(); + ImGui.SetCursorPos(CursorPosStack.Pop()); + return finalCursorPos; + } + + public static void Heading(string text, float scale = 1.25f, bool centered = false) { + var font = ImGui.GetFont(); + var originalScale = font.Scale; + font.Scale *= scale; + ImGui.PushFont(font); + if (centered) ImGuiHelpers.CenteredText(text); + else ImGui.Text(text); + font.Scale = originalScale; + ImGui.PopFont(); + } + + public static bool ClickableHelpMarker(string helpText, float width = 20f) => + ClickableHelpMarker(() => ImGui.TextUnformatted(helpText), width); + + public static bool ClickableHelpMarker(Action tooltipContents, float width = 20f) { + ImGui.SameLine(); + + var originalScale = UiBuilder.IconFont.Scale; + UiBuilder.IconFont.Scale *= 0.60f; + ImGui.PushFont(UiBuilder.IconFont); + ImGui.TextDisabled(FontAwesomeIcon.Question.ToIconString()); + UiBuilder.IconFont.Scale = originalScale; + ImGui.PopFont(); + + var clicked = ImGui.IsItemClicked(); + + if (ImGui.IsItemHovered()) { + ImGui.BeginTooltip(); + ImGui.PushTextWrapPos(ImGui.GetFontSize() * width); + tooltipContents.Invoke(); + ImGui.PopTextWrapPos(); + ImGui.EndTooltip(); + } + + return clicked; + } + + public static bool ToggleBar(string strId, ref uint selection, Vector2 size, params string[] labels) { + var newSelection = selection; + var buttonSize = size with { X = size.X / labels.Length }; + + var buttonConfigs = labels.Select(label => (label, cornerFlags: ImDrawFlags.RoundCornersNone)).ToArray(); + buttonConfigs[0].cornerFlags = ImDrawFlags.RoundCornersLeft; + buttonConfigs[^1].cornerFlags = ImDrawFlags.RoundCornersRight; + + buttonConfigs.ForEach( + (i, buttonConfig) => { + if (0 < i) ImGui.SameLine(0, 0); + ToggleBarButton(buttonConfig.label, i, ref newSelection, buttonSize, buttonConfig.cornerFlags); + } + ); + + var selectionChanged = newSelection != selection; + selection = newSelection; + return selectionChanged; + } + + private static unsafe bool ToggleBarButton( + string label, + uint buttonIndex, + ref uint selection, + Vector2 size, + ImDrawFlags imDrawFlags + ) { + var selected = selection == buttonIndex; + + var baseButtonColor = *ImGui.GetStyleColorVec4(ImGuiCol.Button); + var baseButtonColorActive = *ImGui.GetStyleColorVec4(ImGuiCol.ButtonActive); + var baseButtonColorHovered = *ImGui.GetStyleColorVec4(ImGuiCol.ButtonHovered); + + var buttonColor = selected ? baseButtonColorActive : baseButtonColor; + var buttonColorHovered = selected ? baseButtonColorActive : baseButtonColorHovered with { W = 0.4f }; + + using var scopedColor1 = ImRaii.PushColor(ImGuiCol.Button, buttonColor); + using var scopedColor2 = ImRaii.PushColor(ImGuiCol.ButtonHovered, buttonColorHovered); + + var pressed = ButtonEx(label, size, ImGuiButtonFlags.MouseButtonDefault, imDrawFlags); + + if (pressed) selection = buttonIndex; + + return pressed; + } +} diff --git a/ScoutHelper/Windows/MainWindow.cs b/ScoutHelper/Windows/MainWindow.cs index ee83fc6..a011d37 100644 --- a/ScoutHelper/Windows/MainWindow.cs +++ b/ScoutHelper/Windows/MainWindow.cs @@ -1,23 +1,28 @@ -using CSharpFunctionalExtensions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using CSharpFunctionalExtensions; +using Dalamud.Interface.Components; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; -using Dalamud.Plugin.Services; using ImGuiNET; using ScoutHelper.Localization; using ScoutHelper.Managers; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; +using ScoutHelper.Models; namespace ScoutHelper.Windows; public class MainWindow : Window, IDisposable { + private HuntHelperManager HuntHelperManager { get; } + private BearManager BearManager { get; } + private ConfigWindow ConfigWindow { get; } - private HuntHelperManager HuntHelperManager { get; init; } - private BearManager BearManager { get; init; } + private readonly Vector2 _buttonSize; + private bool _isCopyModeFullText = Plugin.Conf.IsCopyModeFullText; + private uint _selectedMode = Plugin.Conf.IsCopyModeFullText ? 1U : 0U; - public MainWindow(HuntHelperManager huntHelperManager, BearManager bearManager) : base( + public MainWindow(HuntHelperManager huntHelperManager, BearManager bearManager, ConfigWindow configWindow) : base( Strings.MainWindowTitle, ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoScrollbar ) { @@ -26,40 +31,85 @@ public MainWindow(HuntHelperManager huntHelperManager, BearManager bearManager) MaximumSize = new Vector2(float.MaxValue, float.MaxValue), }; + _buttonSize = new[] { + new[] { Strings.BearButton }, + new[] { Strings.SirenButton }, + new[] { Strings.CopyModeLinkButton, Strings.CopyModeFullTextButton, }, + } + .Select( + labels => labels + .Select(ImGuiHelpers.GetButtonSize) + .Aggregate((a, b) => new Vector2(Math.Max(a.X, b.X), Math.Max(a.Y, b.Y))) + ) + .MaxBy(size => size.X); + _buttonSize.X += 4 * ImGui.GetFontSize(); // add some horizontal padding + HuntHelperManager = huntHelperManager; BearManager = bearManager; + ConfigWindow = configWindow; } public void Dispose() { + Plugin.Conf.IsCopyModeFullText = _isCopyModeFullText; + Plugin.Conf.Save(); + GC.SuppressFinalize(this); } public override void Draw() { - var buttonSize = new List { - Strings.BearButton, - Strings.SirenButton - } - .Select(ImGuiHelpers.GetButtonSize) - .MaxBy(size => size.X); - buttonSize.X += ImGui.GetFontSize(); - - if (ImGui.Button(Strings.BearButton, buttonSize)) { - GenerateBearLink(); - } - if (ImGui.IsItemHovered()) { - Utils.CreateTooltip(Strings.BearButtonTooltip); - } + DrawModeButtons(); + + ImGui.Dummy(new Vector2(0, ImGui.GetStyle().FramePadding.Y)); + ImGui.Separator(); + ImGui.Dummy(new Vector2(0, ImGui.GetStyle().FramePadding.Y)); + + DrawGeneratorButtons(); + } + + private void DrawModeButtons() { + ImGuiHelpers.CenteredText(Strings.MainWindowSectionLabelMode); + + ImGui.SameLine(); + if (ImGuiPlus.ClickableHelpMarker(DrawModeTooltipContents)) ConfigWindow.IsOpen = true; + + var modes = new[] { Strings.CopyModeLinkButton, Strings.CopyModeFullTextButton }; + if (ImGuiPlus.ToggleBar("mode", ref _selectedMode, _buttonSize, modes)) + _isCopyModeFullText = _selectedMode == 1; + } + + private static void DrawModeTooltipContents() { + ImGui.TextUnformatted(Strings.CopyModeTooltipSummary); + + ImGui.NewLine(); + + ImGui.TextUnformatted(Strings.CopyModeLinkButton); + ImGui.Indent(); + ImGui.TextUnformatted(Strings.CopyModeTooltipLinkDesc); + ImGui.Unindent(); + + ImGui.NewLine(); + + ImGui.TextUnformatted(Strings.CopyModeFullTextButton); + ImGui.Indent(); + ImGui.TextUnformatted(Strings.CopyModeTooltipFullTextDesc); + ImGui.Unindent(); + } + + private void DrawGeneratorButtons() { + ImGuiHelpers.CenteredText(Strings.MainWindowSectionLabelGenerators); + + if (ImGui.Button(Strings.BearButton, _buttonSize)) GenerateBearLink(); + if (ImGui.IsItemHovered()) Utils.CreateTooltip(Strings.BearButtonTooltip); ImGui.BeginDisabled(true); - if (ImGui.Button(Strings.SirenButton, buttonSize)) { } + ImGui.Button(Strings.SirenButton, _buttonSize); ImGui.EndDisabled(); - if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { - Utils.CreateTooltip(Strings.SirenButtonTooltip); - } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) Utils.CreateTooltip(Strings.SirenButtonTooltip); } - + private void GenerateBearLink() { Plugin.ChatGui.TaggedPrint("Generating Bear link..."); + IList trainList = null!; HuntHelperManager .GetTrainList() @@ -68,20 +118,34 @@ private void GenerateBearLink() { "No mobs in the train :T" ) .Bind( - train => BearManager.GenerateBearLink(Utils.WorldName, train) + train => { + trainList = train; + return BearManager.GenerateBearLink(Utils.WorldName, train); + } ) .ContinueWith( apiResponseTask => { apiResponseTask .Result.Match( bearTrainLink => { - Plugin.ChatGui.TaggedPrint($"Copied link to clipboard: {bearTrainLink.Url}"); - Plugin.ChatGui.TaggedPrint($"Train admin password: {bearTrainLink.Pass}"); - ImGui.SetClipboardText(bearTrainLink.Url); + Plugin.ChatGui.TaggedPrint($"Bear train link: {bearTrainLink.Url}"); + Plugin.ChatGui.TaggedPrint($"Train admin password: {bearTrainLink.Password}"); + if (_isCopyModeFullText) { + var fullText = Utils.FormatTemplate( + Plugin.Conf.CopyTemplate, + trainList, + "bear", + bearTrainLink.HighestPatch, + bearTrainLink.Url + ); + ImGui.SetClipboardText(fullText); + Plugin.ChatGui.TaggedPrint($"Copied full text to clipboard: {fullText}"); + } else { + ImGui.SetClipboardText(bearTrainLink.Url); + Plugin.ChatGui.TaggedPrint("Copied link to clipboard"); + } }, - errorMessage => { - Plugin.ChatGui.TaggedPrintError(errorMessage); - } + errorMessage => { Plugin.ChatGui.TaggedPrintError(errorMessage); } ); } ); diff --git a/ScoutHelper/packages.lock.json b/ScoutHelper/packages.lock.json index 79f2f79..b6c4dfa 100644 --- a/ScoutHelper/packages.lock.json +++ b/ScoutHelper/packages.lock.json @@ -13,6 +13,15 @@ "requested": "[2.1.12, )", "resolved": "2.1.12", "contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg==" + }, + "ottergui": { + "type": "Project", + "dependencies": { + "OtterGuiInternal": "[1.0.0, )" + } + }, + "otterguiinternal": { + "type": "Project" } } }