From 070f501caecaa71c570b30b119dcf666fa030405 Mon Sep 17 00:00:00 2001 From: Wah <74884113+wahtf@users.noreply.github.com> Date: Sun, 26 Jul 2026 16:27:14 -0400 Subject: [PATCH] UI cleanup, Path editor prototype --- .../Converter/QuestWorkConfigConverter.cs | 21 +- Questionable.Model/Questing/DialogueChoice.cs | 2 + .../QuestPathSaveStabilityTest.cs | 31 ++ Questionable/Controller/QuestRegistry.cs | 31 ++ Questionable/DalamudInitializer.cs | 2 + Questionable/QuestionablePlugin.cs | 4 + Questionable/Utils/ImGuiComponentsLocal.cs | 79 +++-- Questionable/Windows/Common/LWindow.cs | 7 + Questionable/Windows/Common/Ui/QstTheme.cs | 102 ++++++ Questionable/Windows/Common/Ui/QstWidgets.cs | 264 ++++++++++++++++ .../ConfigComponents/ConfigComponent.cs | 6 +- .../ConfigComponents/DebugConfigComponent.cs | 20 +- .../ConfigComponents/DutyConfigComponent.cs | 6 +- .../GeneralConfigComponent.cs | 7 +- .../ConfigComponents/PluginConfigComponent.cs | 20 +- .../SinglePlayerDutyConfigComponent.cs | 6 +- .../AlliedSocietyJournalComponent.cs | 32 +- .../GatheringJournalComponent.cs | 8 +- .../QuestJournalComponent.cs | 28 +- .../JournalComponents/QuestJournalUtils.cs | 4 + .../JournalComponents/QuestRewardComponent.cs | 10 +- .../JournalComponents/RedoComponent.cs | 14 +- Questionable/Windows/OneTimeSetupWindow.cs | 8 +- .../PathEditorComponents/PathEditorSession.cs | 64 ++++ .../StepCaptureComponent.cs | 69 +++++ .../PathEditorComponents/StepFormComponent.cs | 187 +++++++++++ Questionable/Windows/PathEditorWindow.cs | 290 ++++++++++++++++++ Questionable/Windows/PriorityWindow.cs | 16 +- .../QuestComponents/ARealmRebornComponent.cs | 8 +- .../QuestComponents/ActiveQuestComponent.cs | 246 +++++++++------ .../QuestComponents/CreationUtilsComponent.cs | 88 ++++-- .../QuestComponents/QuestTooltipComponent.cs | 16 +- .../QuickAccessButtonsComponent.cs | 121 ++------ .../RemainingTasksComponent.cs | 50 ++- .../QuestComponents/ReportWarningComponent.cs | 10 +- Questionable/Windows/QuestSelectionWindow.cs | 8 +- Questionable/Windows/QuestValidationWindow.cs | 16 +- Questionable/Windows/QuestWindow.cs | 41 ++- Questionable/Windows/UiUtils.cs | 30 +- .../Windows/Utils/QuestStepCapture.cs | 37 +++ 40 files changed, 1583 insertions(+), 426 deletions(-) create mode 100644 Questionable.Tests/Serialization/QuestPathSaveStabilityTest.cs create mode 100644 Questionable/Windows/Common/Ui/QstTheme.cs create mode 100644 Questionable/Windows/Common/Ui/QstWidgets.cs create mode 100644 Questionable/Windows/PathEditorComponents/PathEditorSession.cs create mode 100644 Questionable/Windows/PathEditorComponents/StepCaptureComponent.cs create mode 100644 Questionable/Windows/PathEditorComponents/StepFormComponent.cs create mode 100644 Questionable/Windows/PathEditorWindow.cs create mode 100644 Questionable/Windows/Utils/QuestStepCapture.cs diff --git a/Questionable.Model/Questing/Converter/QuestWorkConfigConverter.cs b/Questionable.Model/Questing/Converter/QuestWorkConfigConverter.cs index e3317df27..cb8f32fa6 100644 --- a/Questionable.Model/Questing/Converter/QuestWorkConfigConverter.cs +++ b/Questionable.Model/Questing/Converter/QuestWorkConfigConverter.cs @@ -55,5 +55,24 @@ public override QuestWorkValue Read(ref Utf8JsonReader reader, Type typeToConver throw new JsonException(); } - public override void Write(Utf8JsonWriter writer, QuestWorkValue value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString()); + public override void Write(Utf8JsonWriter writer, QuestWorkValue value, JsonSerializerOptions options) + { + if (value.High != null && value.Low != null && value.Mode == EQuestWorkMode.Bitwise) + writer.WriteNumberValue((byte)((value.High.Value << 4) + value.Low.Value)); + else + { + writer.WriteStartObject(); + if (value.High != null) + writer.WriteNumber(nameof(QuestWorkValue.High), value.High.Value); + if (value.Low != null) + writer.WriteNumber(nameof(QuestWorkValue.Low), value.Low.Value); + if (value.Mode != EQuestWorkMode.Bitwise) + { + writer.WritePropertyName(nameof(QuestWorkValue.Mode)); + new QuestWorkModeConverter().Write(writer, value.Mode, options); + } + + writer.WriteEndObject(); + } + } } diff --git a/Questionable.Model/Questing/DialogueChoice.cs b/Questionable.Model/Questing/DialogueChoice.cs index 301754c9d..594fe9003 100644 --- a/Questionable.Model/Questing/DialogueChoice.cs +++ b/Questionable.Model/Questing/DialogueChoice.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using Questionable.Model.Common; using Questionable.Model.Questing.Converter; namespace Questionable.Model.Questing; @@ -11,6 +12,7 @@ public sealed class DialogueChoice [JsonConverter(typeof(ExcelRefConverter))] public ExcelRef? Prompt { get; set; } + [DefaultTrue] public bool Yes { get; set; } = true; [JsonConverter(typeof(ExcelRefConverter))] diff --git a/Questionable.Tests/Serialization/QuestPathSaveStabilityTest.cs b/Questionable.Tests/Serialization/QuestPathSaveStabilityTest.cs new file mode 100644 index 000000000..7382836f8 --- /dev/null +++ b/Questionable.Tests/Serialization/QuestPathSaveStabilityTest.cs @@ -0,0 +1,31 @@ +using System.Text.Json; +using Questionable.Model.Questing; +using Questionable.Tests.TestData; +using Questionable.Utils; +using Xunit; + +namespace Questionable.Tests.Serialization; + +public sealed class QuestPathSaveStabilityTest +{ + [Theory] + [ClassData(typeof(EmbeddedQuestLoader))] + public void SavingAQuestPathProducesIdenticalJson(EmbeddedQuest embedded) + { + QuestRoot original; + using (var stream = embedded.OpenStream()) + { + original = JsonSerializer.Deserialize(stream) + ?? throw new Xunit.Sdk.XunitException( + $"'{embedded.ManifestName}' deserialized to a null QuestRoot."); + } + + string once = JsonSerializer.Serialize(original, JsonOptions.Default); + QuestRoot reloaded = JsonSerializer.Deserialize(once) + ?? throw new Xunit.Sdk.XunitException( + $"Re-deserializing '{embedded.ManifestName}' returned null."); + string twice = JsonSerializer.Serialize(reloaded, JsonOptions.Default); + + Assert.Equal(once, twice); + } +} diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs index 5029c4264..8b47caf9d 100644 --- a/Questionable/Controller/QuestRegistry.cs +++ b/Questionable/Controller/QuestRegistry.cs @@ -531,6 +531,37 @@ public static (bool, FileInfo?, string) CreatePath(QuestInfo info, Quest? quest return (true, file, $"File created{(dryrun ? " (dry run)" : "")}"); } + public static (bool Success, string Message) SaveUserPath(QuestInfo info, QuestRoot root) + { + string directory = Path.Combine(Svc.PluginInterface.GetPluginConfigDirectory(), "Quests"); + Directory.CreateDirectory(directory); + string path = Path.Combine(directory, GetFilename(info)); + + JsonObject serialized = (JsonObject)JsonSerializer.SerializeToNode(root, JsonOptions.Default)!; + JsonObject withSchema = new() + { + { + "$schema", + "https://qstxiv.github.io/schema/quest-v1.json" + } + }; + foreach ((string key, JsonNode? value) in serialized) + withSchema.Add(key, value?.DeepClone()); + + using (FileStream stream = new(path, FileMode.Create)) + using (Utf8JsonWriter writer = new(stream, new() + { + Encoder = JsonOptions.Default.Encoder, + Indented = JsonOptions.Default.WriteIndented + })) + { + withSchema.WriteTo(writer, JsonOptions.Default); + } + + Svc.Log.Information($"Saved user quest path to {path}"); + return (true, path); + } + public static string OpenEditorDescription = _L("Clicking this button writes the quest path to a file and opens it in your default text editor.") + _L("After making a change, click Reload Data below.") + "\n" + _L("To revert to the official version, delete the file and click Reload Data again.") + "\n" + diff --git a/Questionable/DalamudInitializer.cs b/Questionable/DalamudInitializer.cs index 8fb6915fe..2ef8a638a 100644 --- a/Questionable/DalamudInitializer.cs +++ b/Questionable/DalamudInitializer.cs @@ -34,6 +34,7 @@ public DalamudInitializer( QuestValidationWindow questValidationWindow, JournalProgressWindow journalProgressWindow, PriorityWindow priorityWindow, + PathEditorWindow pathEditorWindow, IChatGui chatGui, IToastGui toastGui, Configuration configuration, @@ -65,6 +66,7 @@ public DalamudInitializer( _windowSystem.AddWindow(questValidationWindow); _windowSystem.AddWindow(journalProgressWindow); _windowSystem.AddWindow(priorityWindow); + _windowSystem.AddWindow(pathEditorWindow); _pluginInterface.UiBuilder.Draw += _windowSystem.Draw; _pluginInterface.UiBuilder.OpenMainUi += ToggleQuestWindow; diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index 09d7de4ad..67acaed27 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -358,6 +358,10 @@ private static void AddWindows(ServiceCollection serviceCollection) serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Questionable/Utils/ImGuiComponentsLocal.cs b/Questionable/Utils/ImGuiComponentsLocal.cs index c8b64fdc8..7288296b4 100644 --- a/Questionable/Utils/ImGuiComponentsLocal.cs +++ b/Questionable/Utils/ImGuiComponentsLocal.cs @@ -96,54 +96,53 @@ internal static bool DrawSearchableCombo( var size = ImGui.GetWindowContentRegionMax(); ImGui.SetNextItemWidth(size.X / 2); } - if (ImGui.BeginCombo($"{(!labelAsPreview ? label : "")}##SearchableCombo:{Path.GetFileName(file)}:{line}", preview, ImGuiComboFlags.HeightLarge)) + using var combo = ImRaii.Combo($"{(!labelAsPreview ? label : "")}##SearchableCombo:{Path.GetFileName(file)}:{line}", preview, ImGuiComboFlags.HeightLarge); + if (!combo) + return false; + + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.IsWindowAppearing()) + ImGui.SetKeyboardFocusHere(); + ImGui.InputTextWithHint("##filter", "Search...", ref searchString, 256); + + // The option list lives in its own fixed-height scrollable child so the search box above + // stays pinned and visible; SetItemDefaultFocus() then scrolls the child, not the popup. + int visibleRows = Math.Clamp(labels.Length, 1, 12); + var listSize = ImGui.GetContentRegionAvail() with { Y = ImGui.GetTextLineHeightWithSpacing() * visibleRows }; + using (var child = ImRaii.Child("##searchableComboList", listSize)) { - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); - if (ImGui.IsWindowAppearing()) - ImGui.SetKeyboardFocusHere(); - ImGui.InputTextWithHint("##filter", "Search...", ref searchString, 256); - - // The option list lives in its own fixed-height scrollable child so the search box above - // stays pinned and visible; SetItemDefaultFocus() then scrolls the child, not the popup. - int visibleRows = Math.Clamp(labels.Length, 1, 12); - var listSize = ImGui.GetContentRegionAvail() with { Y = ImGui.GetTextLineHeightWithSpacing() * visibleRows }; - using (var child = ImRaii.Child("##searchableComboList", listSize)) + if (child) { - if (child) + for (int i = 0; i < labels.Length; i++) { - for (int i = 0; i < labels.Length; i++) + if (!string.IsNullOrEmpty(searchString) && + !labels[i].Contains(searchString, StringComparison.CurrentCultureIgnoreCase)) + continue; + if (labels[i].StartsWith("##D")) { - if (!string.IsNullOrEmpty(searchString) && - !labels[i].Contains(searchString, StringComparison.CurrentCultureIgnoreCase)) - continue; - if (labels[i].StartsWith("##D")) - { - ImGui.TextDisabled(labels[i].Substring(3)); - continue; - } - if (labels[i].StartsWith("##S")) - { - ImGui.Separator(); - continue; - } - - bool isSelected = i == index; - if (ImGui.Selectable(labels[i], isSelected)) - { - selected = values[i]; - searchString = string.Empty; - } - - if (isSelected) - ImGui.SetItemDefaultFocus(); + ImGui.TextDisabled(labels[i].Substring(3)); + continue; } + if (labels[i].StartsWith("##S")) + { + ImGui.Separator(); + continue; + } + + bool isSelected = i == index; + if (ImGui.Selectable(labels[i], isSelected)) + { + selected = values[i]; + searchString = string.Empty; + } + + if (isSelected) + ImGui.SetItemDefaultFocus(); } } - - ImGui.EndCombo(); - return true; } - return false; + + return true; } public static void HelpMarker(string helpText, string[]? bullets) => HelpMarker(helpText, FontAwesomeIcon.InfoCircle, bullets: bullets); diff --git a/Questionable/Windows/Common/LWindow.cs b/Questionable/Windows/Common/LWindow.cs index 9525ee761..1e7af1430 100644 --- a/Questionable/Windows/Common/LWindow.cs +++ b/Questionable/Windows/Common/LWindow.cs @@ -1,10 +1,12 @@ using Dalamud.Bindings.ImGui; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows.Common; public abstract class LWindow(string windowName, ImGuiWindowFlags flags = ImGuiWindowFlags.None, bool forceMainWindow = false) : Window(windowName, flags, forceMainWindow) { private bool _initializedConfig; private bool _wasCollapsedLastFrame; + private QstTheme.WindowStyleScope? _windowStyle; protected bool ClickedHeaderLastFrame { get; private set; } protected bool ClickedHeaderCurrentFrame { get; private set; } @@ -98,6 +100,8 @@ public override void OnOpen() public override void PreDraw() { + _windowStyle = QstTheme.PushWindowStyle(); + if (!_initializedConfig) LoadWindowConfig(); @@ -124,6 +128,9 @@ public sealed override void Draw() public override void PostDraw() { + _windowStyle?.Dispose(); + _windowStyle = null; + base.PostDraw(); if (_initializedConfig) diff --git a/Questionable/Windows/Common/Ui/QstTheme.cs b/Questionable/Windows/Common/Ui/QstTheme.cs new file mode 100644 index 000000000..d8b22c5ed --- /dev/null +++ b/Questionable/Windows/Common/Ui/QstTheme.cs @@ -0,0 +1,102 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Utility.Raii; + +namespace Questionable.Windows.Common.Ui; + +internal static class QstTheme +{ + public static readonly Vector4 Accent = Rgb(255, 148, 68); + public static readonly Vector4 AccentActive = Rgb(255, 167, 94); + public static readonly Vector4 Danger = Rgb(255, 81, 110); + public static readonly Vector4 Amber = Rgb(255, 176, 102); + public static readonly Vector4 Success = Rgb(126, 212, 145); + public static readonly Vector4 Info = Rgb(111, 174, 230); + public static readonly Vector4 Special = Rgb(181, 144, 232); + public static readonly Vector4 Text = Rgb(224, 224, 224); + public static readonly Vector4 TextMuted = Rgb(156, 163, 175); + public static readonly Vector4 TextFaint = Rgb(117, 123, 134); + + public static readonly Vector4 PanelBg = Rgba(26, 26, 26, 0.96f); + public static readonly Vector4 PanelDark = Rgb(14, 14, 14); + public static readonly Vector4 InputBg = Rgb(19, 19, 19); + public static readonly Vector4 Raised = Rgb(38, 38, 38); + public static readonly Vector4 RaisedHovered = Rgb(48, 48, 48); + public static readonly Vector4 RaisedActive = Rgb(58, 58, 58); + public static readonly Vector4 Edge = Rgb(46, 46, 46); + + public static Vector4 WithAlpha(Vector4 color, float alpha) => color with { W = alpha }; + + public static uint ToU32(Vector4 color) => ImGui.ColorConvertFloat4ToU32(color); + + public static WindowStyleScope PushWindowStyle() => new(); + + internal sealed class WindowStyleScope : IDisposable + { + private readonly ImRaii.ColorDisposable _colors; + private readonly ImRaii.StyleDisposable _styles; + + internal WindowStyleScope() + { + _colors = ImRaii.PushColor(ImGuiCol.WindowBg, PanelBg) + .Push(ImGuiCol.PopupBg, Rgba(27, 27, 27, 0.98f)) + .Push(ImGuiCol.Border, Edge) + .Push(ImGuiCol.Separator, Edge) + .Push(ImGuiCol.FrameBg, InputBg) + .Push(ImGuiCol.FrameBgHovered, Rgb(31, 31, 31)) + .Push(ImGuiCol.FrameBgActive, Raised) + .Push(ImGuiCol.TitleBg, PanelDark) + .Push(ImGuiCol.TitleBgActive, Rgb(26, 26, 26)) + .Push(ImGuiCol.TitleBgCollapsed, Rgba(14, 14, 14, 0.8f)) + .Push(ImGuiCol.Button, Raised) + .Push(ImGuiCol.ButtonHovered, RaisedHovered) + .Push(ImGuiCol.ButtonActive, RaisedActive) + .Push(ImGuiCol.Header, Raised) + .Push(ImGuiCol.HeaderHovered, RaisedHovered) + .Push(ImGuiCol.HeaderActive, RaisedActive) + .Push(ImGuiCol.Tab, Rgb(26, 26, 26)) + .Push(ImGuiCol.TabHovered, RaisedHovered) + .Push(ImGuiCol.TabActive, Rgb(43, 43, 43)) + .Push(ImGuiCol.TabUnfocused, Rgb(26, 26, 26)) + .Push(ImGuiCol.TabUnfocusedActive, Rgb(36, 36, 36)) + .Push(ImGuiCol.CheckMark, Accent) + .Push(ImGuiCol.SliderGrab, Accent) + .Push(ImGuiCol.SliderGrabActive, AccentActive) + .Push(ImGuiCol.ScrollbarBg, Rgba(19, 19, 19, 0.5f)) + .Push(ImGuiCol.ScrollbarGrab, Rgb(52, 52, 52)) + .Push(ImGuiCol.ScrollbarGrabHovered, Rgb(70, 70, 70)) + .Push(ImGuiCol.ScrollbarGrabActive, Rgb(86, 86, 86)) + .Push(ImGuiCol.TextSelectedBg, WithAlpha(Accent, 0.35f)) + .Push(ImGuiCol.Text, Text) + .Push(ImGuiCol.TextDisabled, TextFaint); + + _styles = ImRaii.PushStyle(ImGuiStyleVar.WindowRounding, 8f) + .Push(ImGuiStyleVar.ChildRounding, 6f) + .Push(ImGuiStyleVar.FrameRounding, 5f) + .Push(ImGuiStyleVar.PopupRounding, 6f) + .Push(ImGuiStyleVar.GrabRounding, 4f) + .Push(ImGuiStyleVar.TabRounding, 5f) + .Push(ImGuiStyleVar.ScrollbarRounding, 6f) + .Push(ImGuiStyleVar.ScrollbarSize, 10f) + .Push(ImGuiStyleVar.WindowBorderSize, 1f) + .Push(ImGuiStyleVar.WindowPadding, new Vector2(10, 9)) + .Push(ImGuiStyleVar.FramePadding, new Vector2(6, 4)) + .Push(ImGuiStyleVar.ItemSpacing, new Vector2(7, 5)); + } + + public void Dispose() + { + _styles.Dispose(); + _colors.Dispose(); + } + } + + private static Vector4 Rgb(byte red, byte green, byte blue) + { + return new Vector4(red / 255f, green / 255f, blue / 255f, 1f); + } + + private static Vector4 Rgba(byte red, byte green, byte blue, float alpha) + { + return new Vector4(red / 255f, green / 255f, blue / 255f, alpha); + } +} diff --git a/Questionable/Windows/Common/Ui/QstWidgets.cs b/Questionable/Windows/Common/Ui/QstWidgets.cs new file mode 100644 index 000000000..d32d6c048 --- /dev/null +++ b/Questionable/Windows/Common/Ui/QstWidgets.cs @@ -0,0 +1,264 @@ +using System.Globalization; +using System.Runtime.CompilerServices; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Questionable.Utils; + +namespace Questionable.Windows.Common.Ui; + +internal static class QstWidgets +{ + // Collapsible section header with an optional count. + public static bool SectionHeader(string label, string id, int? count = null, bool defaultOpen = true) + { + ImGuiTreeNodeFlags flags = defaultOpen ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None; + + using ImRaii.ColorDisposable headerColor = + ImRaii.PushColor(ImGuiCol.Header, QstTheme.WithAlpha(QstTheme.Text, 0f)); + using ImRaii.ColorDisposable hoverColor = + ImRaii.PushColor(ImGuiCol.HeaderHovered, QstTheme.WithAlpha(QstTheme.Text, 0.06f)); + using ImRaii.ColorDisposable activeColor = + ImRaii.PushColor(ImGuiCol.HeaderActive, QstTheme.WithAlpha(QstTheme.Text, 0.09f)); + using ImRaii.ColorDisposable textColor = ImRaii.PushColor(ImGuiCol.Text, QstTheme.TextMuted); + + bool open = ImGui.CollapsingHeader($"{label.ToUpper(CultureInfo.CurrentUICulture)}###{id}", flags); + + if (count != null) + { + string countText = count.Value.ToString(CultureInfo.CurrentCulture); + Vector2 textSize = ImGui.CalcTextSize(countText); + Vector2 min = ImGui.GetItemRectMin(); + Vector2 max = ImGui.GetItemRectMax(); + Vector2 pos = new( + max.X - textSize.X - ImGui.GetStyle().FramePadding.X * 2, + min.Y + (max.Y - min.Y - textSize.Y) / 2f); + ImGui.GetWindowDrawList().AddText(pos, QstTheme.ToU32(QstTheme.TextMuted), countText); + } + + return open; + } + + // Status pill in the window title bar. + public static void TitleBarPill(string text, Vector4 color, string windowTitle) + { + float frameHeight = ImGui.GetFrameHeight(); + Vector2 windowPos = ImGui.GetWindowPos(); + float windowWidth = ImGui.GetWindowWidth(); + float scale = ImGuiHelpers.GlobalScale; + + Vector2 textSize = ImGui.CalcTextSize(text); + float dotRadius = 3f * scale; + Vector2 padding = new(7f * scale, 1.5f * scale); + float height = Math.Min(textSize.Y + padding.Y * 2f, frameHeight - 4f * scale); + float width = padding.X * 2f + dotRadius * 2f + 4f * scale + textSize.X; + + string visibleTitle = windowTitle.Split("###")[0]; + float titleStart = ImGui.GetFontSize() + ImGui.GetStyle().FramePadding.X + + ImGui.GetStyle().ItemInnerSpacing.X; + Vector2 topLeft = new( + windowPos.X + titleStart + ImGui.CalcTextSize(visibleTitle).X + 8f * scale, + windowPos.Y + (frameHeight - height) / 2f); + + ImDrawListPtr drawList = ImGui.GetWindowDrawList(); + drawList.PushClipRect(windowPos, windowPos + new Vector2(windowWidth, frameHeight), false); + drawList.AddRectFilled(topLeft, topLeft + new Vector2(width, height), + QstTheme.ToU32(QstTheme.WithAlpha(color, 0.15f)), height / 2f); + drawList.AddCircleFilled(new Vector2(topLeft.X + padding.X + dotRadius, topLeft.Y + height / 2f), + dotRadius, QstTheme.ToU32(color)); + drawList.AddText(new Vector2(topLeft.X + padding.X + dotRadius * 2f + 4f * scale, + topLeft.Y + (height - textSize.Y) / 2f), QstTheme.ToU32(color), text); + drawList.PopClipRect(); + } + + // Inline status pill. + public static void StatusPill(string text, Vector4 color) + { + float scale = ImGuiHelpers.GlobalScale; + Vector2 textSize = ImGui.CalcTextSize(text); + float dotRadius = 3f * scale; + Vector2 padding = new(8f * scale, 2f * scale); + float height = textSize.Y + padding.Y * 2; + float width = padding.X * 2 + dotRadius * 2 + 4f * scale + textSize.X; + + Vector2 pos = ImGui.GetCursorScreenPos(); + ImDrawListPtr drawList = ImGui.GetWindowDrawList(); + drawList.AddRectFilled(pos, pos + new Vector2(width, height), + QstTheme.ToU32(QstTheme.WithAlpha(color, 0.15f)), height / 2f); + drawList.AddCircleFilled(new Vector2(pos.X + padding.X + dotRadius, pos.Y + height / 2f), + dotRadius, QstTheme.ToU32(color)); + drawList.AddText(new Vector2(pos.X + padding.X + dotRadius * 2 + 4f * scale, pos.Y + padding.Y), + QstTheme.ToU32(color), text); + ImGui.Dummy(new Vector2(width, height)); + } + + // Slim progress bar. + public static void ThinProgressBar(float fraction, Vector4 color, float height = 4f) + { + float barHeight = height * ImGuiHelpers.GlobalScale; + float width = ImGui.GetContentRegionAvail().X; + float rounding = barHeight / 2f; + + Vector2 pos = ImGui.GetCursorScreenPos(); + ImDrawListPtr drawList = ImGui.GetWindowDrawList(); + drawList.AddRectFilled(pos, pos + new Vector2(width, barHeight), + QstTheme.ToU32(QstTheme.WithAlpha(color, 0.2f)), rounding); + + float clamped = Math.Clamp(fraction, 0f, 1f); + if (clamped > 0f) + drawList.AddRectFilled(pos, pos + new Vector2(width * clamped, barHeight), + QstTheme.ToU32(color), rounding); + + ImGui.Dummy(new Vector2(width, barHeight)); + } + + // Small rounded label. + public static bool Chip(string text, Vector4 color) + { + float scale = ImGuiHelpers.GlobalScale; + Vector2 textSize = ImGui.CalcTextSize(text); + Vector2 padding = new(6f * scale, 1f * scale); + Vector2 size = textSize + padding * 2; + + Vector2 pos = ImGui.GetCursorScreenPos(); + ImDrawListPtr drawList = ImGui.GetWindowDrawList(); + drawList.AddRectFilled(pos, pos + size, QstTheme.ToU32(QstTheme.WithAlpha(color, 0.13f)), size.Y / 2f); + drawList.AddText(pos + padding, QstTheme.ToU32(color), text); + ImGui.Dummy(size); + return ImGui.IsItemHovered(); + } + + // Icon button with a tooltip and an optional count badge. + public static bool RailButton(FontAwesomeIcon icon, string tooltip, Vector4? tint = null, + bool enabled = true, string? countBadge = null, Vector4? badgeColor = null, + [CallerFilePath] string file = "", [CallerLineNumber] int line = 0) + { + float size = ImGui.GetFrameHeight(); + bool clicked; + using (ImRaii.Disabled(!enabled)) + { + clicked = ImGuiComponentsLocal.IconButton(icon, tint, activeColor: null, hoveredColor: null, + new Vector2(size, size), file, line); + } + + if (countBadge != null) + { + Vector2 max = ImGui.GetItemRectMax(); + Vector2 badgeSize = ImGui.CalcTextSize(countBadge); + Vector2 pos = new(max.X - badgeSize.X - 1f, max.Y - badgeSize.Y + 2f); + ImGui.GetWindowDrawList().AddText(pos, QstTheme.ToU32(badgeColor ?? QstTheme.TextMuted), countBadge); + } + + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + { + using ImRaii.TooltipDisposable _ = ImRaii.Tooltip(); + ImGui.TextUnformatted(tooltip); + } + + return clicked && enabled; + } + + // Boxed group of SegmentToggles. + public static SegmentGroupScope SegmentGroup(bool armed) => new(armed); + + // Inset content box. + public static CardScope Card(Vector4? borderColor = null) => new(borderColor); + + internal sealed class CardScope : IDisposable + { + private readonly Vector2 _topLeft; + private readonly float _width; + private readonly float _padding; + private readonly Vector4 _borderColor; + + internal CardScope(Vector4? borderColor) + { + _padding = 7f * ImGuiHelpers.GlobalScale; + _topLeft = ImGui.GetCursorScreenPos(); + _width = ImGui.GetContentRegionAvail().X; + _borderColor = borderColor ?? QstTheme.Edge; + + ImDrawListPtr drawList = ImGui.GetWindowDrawList(); + drawList.ChannelsSplit(2); + drawList.ChannelsSetCurrent(1); + ImGui.SetCursorScreenPos(_topLeft + new Vector2(_padding, _padding)); + ImGui.BeginGroup(); + } + + public void Dispose() + { + ImGui.EndGroup(); + Vector2 contentMax = ImGui.GetItemRectMax(); + Vector2 bottomRight = new(_topLeft.X + _width, contentMax.Y + _padding); + + ImDrawListPtr drawList = ImGui.GetWindowDrawList(); + drawList.ChannelsSetCurrent(0); + float rounding = 6f * ImGuiHelpers.GlobalScale; + drawList.AddRectFilled(_topLeft, bottomRight, QstTheme.ToU32(QstTheme.InputBg), rounding); + drawList.AddRect(_topLeft, bottomRight, QstTheme.ToU32(_borderColor), rounding); + drawList.ChannelsMerge(); + + ImGui.SetCursorScreenPos(new Vector2(_topLeft.X, bottomRight.Y)); + ImGui.Dummy(new Vector2(_width, 0)); + } + } + + // Toggle button for a SegmentGroup. + public static bool SegmentToggle(FontAwesomeIcon icon, bool active, string tooltipOn, string tooltipOff, + [CallerFilePath] string file = "", [CallerLineNumber] int line = 0) + { + float size = ImGui.GetFrameHeight(); + Vector4? tint = active ? QstTheme.Accent : null; + bool clicked = ImGuiComponentsLocal.IconButton(icon, tint, activeColor: null, hoveredColor: null, + new Vector2(size, size), file, line); + + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + { + using ImRaii.TooltipDisposable _ = ImRaii.Tooltip(); + ImGui.TextUnformatted(active ? tooltipOn : tooltipOff); + } + + return clicked; + } + + internal sealed class SegmentGroupScope : IDisposable + { + private readonly ImRaii.StyleDisposable _spacing; + private readonly ImRaii.ColorDisposable? _armedTint; + private readonly bool _armed; + + internal SegmentGroupScope(bool armed) + { + _armed = armed; + ImDrawListPtr drawList = ImGui.GetWindowDrawList(); + drawList.ChannelsSplit(2); + drawList.ChannelsSetCurrent(1); + + _spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + new Vector2(1f * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y)); + if (armed) + _armedTint = ImRaii.PushColor(ImGuiCol.Button, QstTheme.WithAlpha(QstTheme.Accent, 0.15f)); + ImGui.BeginGroup(); + } + + public void Dispose() + { + ImGui.EndGroup(); + _armedTint?.Dispose(); + _spacing.Dispose(); + + float inset = 2f * ImGuiHelpers.GlobalScale; + Vector2 min = ImGui.GetItemRectMin() - new Vector2(inset, inset); + Vector2 max = ImGui.GetItemRectMax() + new Vector2(inset, inset); + + ImDrawListPtr drawList = ImGui.GetWindowDrawList(); + drawList.ChannelsSetCurrent(0); + float rounding = 6f * ImGuiHelpers.GlobalScale; + drawList.AddRectFilled(min, max, QstTheme.ToU32(QstTheme.InputBg), rounding); + drawList.AddRect(min, max, + QstTheme.ToU32(_armed ? QstTheme.WithAlpha(QstTheme.Accent, 0.55f) : QstTheme.Edge), rounding); + drawList.ChannelsMerge(); + } + } +} diff --git a/Questionable/Windows/ConfigComponents/ConfigComponent.cs b/Questionable/Windows/ConfigComponents/ConfigComponent.cs index 28f6c37c0..bc1cb226c 100644 --- a/Questionable/Windows/ConfigComponents/ConfigComponent.cs +++ b/Questionable/Windows/ConfigComponents/ConfigComponent.cs @@ -1,8 +1,8 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.Text; using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility.Raii; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows.ConfigComponents; internal abstract class ConfigComponent(IDalamudPluginInterface pluginInterface, Configuration configuration) @@ -67,7 +67,7 @@ protected static string FormatLevel(int level, bool includePrefix = true) protected static void DrawNotes(bool enabledByDefault, IEnumerable notes) { - using ImRaii.ColorDisposable color = ImRaii.PushColor(ImGuiCol.TextDisabled, !enabledByDefault ? ImGuiColors.DalamudYellow : ImGuiColors.ParsedBlue); + using ImRaii.ColorDisposable color = ImRaii.PushColor(ImGuiCol.TextDisabled, !enabledByDefault ? QstTheme.Amber : QstTheme.Info); ImGui.SameLine(); using (ImRaii.PushFont(UiBuilder.IconFont)) @@ -83,7 +83,7 @@ protected static void DrawNotes(bool enabledByDefault, IEnumerable notes using ImRaii.TooltipDisposable _ = ImRaii.Tooltip(); - ImGui.TextColored(ImGuiColors.DalamudYellow, + ImGui.TextColored(QstTheme.Amber, _L("While testing, the following issues have been found:")); foreach (string note in notes) ImGui.BulletText(note); diff --git a/Questionable/Windows/ConfigComponents/DebugConfigComponent.cs b/Questionable/Windows/ConfigComponents/DebugConfigComponent.cs index e5c13355c..74070258b 100644 --- a/Questionable/Windows/ConfigComponents/DebugConfigComponent.cs +++ b/Questionable/Windows/ConfigComponents/DebugConfigComponent.cs @@ -1,10 +1,10 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Utility.Raii; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Lumina.Excel.Sheets; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows.ConfigComponents; // TODO: refactor — heavy nesting (41 lines indented ≥6 levels, max indent ~12 levels). @@ -25,7 +25,7 @@ public override void DrawTab() if (!tab) return; - ImGui.TextColored(ImGuiColors.DalamudRed, + ImGui.TextColored(QstTheme.Danger, _L("Enabling any option here may cause unexpected behavior. Use at your own risk.")); ImGui.Separator(); @@ -50,22 +50,22 @@ public override void DrawTab() { using (ImRaii.PushIndent()) { - ImGui.TextColored(ImGuiColors.DalamudOrange, + ImGui.TextColored(QstTheme.Accent, _L("Generated paths are unreviewed machine drafts: expect wrong targets, missing steps and stalls.")); - ImGui.TextColored(ImGuiColors.DalamudOrange, + ImGui.TextColored(QstTheme.Accent, _L("Stay at the keyboard while one is running - never leave it unattended.")); ImGui.TextUnformatted( _L("Right-click a quest without a path in the Journal Progress window to generate a draft.")); if (!draftQuestPathService.UserDirectoryIsLoaded) { - ImGui.TextColored(ImGuiColors.DalamudRed, + ImGui.TextColored(QstTheme.Danger, _L("Generated paths only load in debug mode or on a dev install; without one of those, this option does nothing.")); } } } - if (ImGui.CollapsingHeader(_L("Information"))) + if (QstWidgets.SectionHeader(_L("Information"), "Information", defaultOpen: false)) { using (ImRaii.PushIndent()) { @@ -187,7 +187,7 @@ public override void DrawTab() ImGui.Separator(); - if (ImGui.CollapsingHeader(_L("Reward item redemption"))) + if (QstWidgets.SectionHeader(_L("Reward item redemption"), "RewardRedemption", defaultOpen: false)) { using (ImRaii.PushIndent()) { @@ -269,7 +269,7 @@ public override void DrawTab() } ImGui.Separator(); - if (ImGui.CollapsingHeader(_L("Quest/Interaction Skips"))) + if (QstWidgets.SectionHeader(_L("Quest/Interaction Skips"), "QuestSkips", defaultOpen: false)) { using (ImRaii.PushIndent()) { @@ -404,10 +404,10 @@ public override void DrawTab() pathDataUpdater.CheckForUpdatesManually(); ImGui.SameLine(); - ImGui.TextColored(ImGuiColors.DalamudGrey, pathDataUpdater.Status); + ImGui.TextColored(QstTheme.TextMuted, pathDataUpdater.Status); long installedVersion = Configuration.PathData.InstalledDataVersion; - ImGui.TextColored(ImGuiColors.DalamudGrey, + ImGui.TextColored(QstTheme.TextMuted, installedVersion == 0 ? _L("Using the path data bundled with the plugin.") : _LF("Downloaded path data version: {0}", installedVersion)); diff --git a/Questionable/Windows/ConfigComponents/DutyConfigComponent.cs b/Questionable/Windows/ConfigComponents/DutyConfigComponent.cs index 68d5f6cb3..516c6dce6 100644 --- a/Questionable/Windows/ConfigComponents/DutyConfigComponent.cs +++ b/Questionable/Windows/ConfigComponents/DutyConfigComponent.cs @@ -1,13 +1,13 @@ using System.Text; using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; using Lumina.Excel.Sheets; using Questionable.Model.Common; using Questionable.Model.Questing; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows.ConfigComponents; internal sealed class DutyConfigComponent : ConfigComponent @@ -88,7 +88,7 @@ public override void DrawTab() "a duty's required item level, Questionable will ask AutoDuty to run it solo as an Unrestricted Party.") + _L("This now does not include Trials, which are likely to have extra complexity or mechanics making them infeasible to complete as an Unrestricted Party.")); ImGui.SameLine(); - ImGui.TextColored(ImGuiColors.DalamudRed, _L("Experimental feature")); + ImGui.TextColored(QstTheme.Danger, _L("Experimental feature")); } ImGui.Separator(); @@ -204,7 +204,7 @@ private void DrawDutyNameCell(DutyInfo dutyInfo, DutyOptions dutyOptions) if (runInstancedContentWithAutoDuty && !_autoDutyIpc.HasPath(dutyInfo.CfcId)) { ImGuiComponents.HelpMarker(_L("This duty is not supported by AutoDuty"), - FontAwesomeIcon.Times, ImGuiColors.DalamudRed); + FontAwesomeIcon.Times, QstTheme.Danger); } else if (dutyOptions.Notes.Count > 0) DrawNotes(dutyOptions.Enabled, dutyOptions.Notes); diff --git a/Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs b/Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs index 3d5c1ded5..e21e2f252 100644 --- a/Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs +++ b/Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs @@ -8,6 +8,7 @@ using Questionable.Model.Questing; using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows.ConfigComponents; internal sealed class GeneralConfigComponent : ConfigComponent @@ -117,7 +118,7 @@ public override void DrawTab() DalamudInitializer.SetupI18N(Configuration.General.Language); } - if (ImGui.CollapsingHeader(_L("Preferences"))) + if (QstWidgets.SectionHeader(_L("Preferences"), "Preferences", defaultOpen: false)) { ECombatModule combatModule = Configuration.General.CombatModule; ImGui.SetNextItemWidth(size.X / 2); @@ -228,7 +229,7 @@ public override void DrawTab() } } - if (ImGui.CollapsingHeader(_L("UI"))) + if (QstWidgets.SectionHeader(_L("UI"), "UI", defaultOpen: false)) { using (ImRaii.PushIndent()) { @@ -301,7 +302,7 @@ public override void DrawTab() } #endif - if (ImGui.CollapsingHeader(_L("Questing"))) + if (QstWidgets.SectionHeader(_L("Questing"), "Questing", defaultOpen: false)) { using (ImRaii.PushIndent()) { diff --git a/Questionable/Windows/ConfigComponents/PluginConfigComponent.cs b/Questionable/Windows/ConfigComponents/PluginConfigComponent.cs index 2173bc815..a2e65e98b 100644 --- a/Questionable/Windows/ConfigComponents/PluginConfigComponent.cs +++ b/Questionable/Windows/ConfigComponents/PluginConfigComponent.cs @@ -1,13 +1,13 @@ -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; using ECommons.ImGuiMethods; using Questionable.Model.Common; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows.ConfigComponents; internal sealed class PluginConfigComponent @@ -157,10 +157,10 @@ public override void DrawTab() ImGui.Spacing(); if (allRequiredInstalled) - ImGui.TextColored(ImGuiColors.ParsedGreen, _L("All required plugins are installed.")); + ImGui.TextColored(QstTheme.Success, _L("All required plugins are installed.")); else { - ImGui.TextColored(ImGuiColors.DalamudRed, + ImGui.TextColored(QstTheme.Danger, _L("Required plugins are missing, Questionable will not work properly.")); } } @@ -176,7 +176,7 @@ public void Draw(out bool allRequiredInstalled) allRequiredInstalled = true; ImGui.SetNextItemOpen(isOpen: true, ImGuiCond.Once); - if (ImGui.CollapsingHeader(_L("Required plugins:"))) + if (QstWidgets.SectionHeader(_L("Required plugins:"), "RequiredPlugins", defaultOpen: false)) { using (ImRaii.PushIndent()) { @@ -185,7 +185,7 @@ public void Draw(out bool allRequiredInstalled) } } - if (ImGui.CollapsingHeader(_L("Rotation/Automation plugins:"))) + if (QstWidgets.SectionHeader(_L("Rotation/Automation plugins:"), "RotationPlugins", defaultOpen: false)) { using (ImRaii.Disabled(_combatController.IsRunning)) { @@ -211,7 +211,7 @@ public void Draw(out bool allRequiredInstalled) } } - if (ImGui.CollapsingHeader(_L("Recommended/niche plugins:"))) + if (QstWidgets.SectionHeader(_L("Recommended/niche plugins:"), "NichePlugins", defaultOpen: false)) { using (ImRaii.PushIndent()) { @@ -287,7 +287,7 @@ private bool DrawCombatPlugin(ECombatModule combatModule, float checklistPadding ImGui.SameLine(0); using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) { - Vector4 iconColor = isInstalled ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; + Vector4 iconColor = isInstalled ? QstTheme.Success : QstTheme.Danger; FontAwesomeIcon icon = isInstalled ? FontAwesomeIcon.Check : FontAwesomeIcon.Times; ImGui.AlignTextToFramePadding(); @@ -334,7 +334,7 @@ private void DrawPluginDetails(PluginInfo plugin, float checklistPadding, bool i { ImRaii.ColorDisposable? color = null; if (!allDetailsOk) - color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange); + color = ImRaii.PushColor(ImGuiCol.Text, QstTheme.Accent); if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.Cog)) _commandManager.ProcessCommand(plugin.ConfigCommand); color?.Dispose(); @@ -369,7 +369,7 @@ private static bool PluginImageButton(PluginInfo plugin, float size, bool isInst logo.Handle, new(size.Scale(), size.Scale()), 2, - isInstalled ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed, + isInstalled ? QstTheme.Success : QstTheme.Danger, isActive ? Vector4.One : new(0.5f, 0.5f, 0.5f, 1f) ); } diff --git a/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs b/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs index 1a98682ef..0db36fe29 100644 --- a/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs +++ b/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs @@ -3,7 +3,6 @@ using System.Text; using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Utility.Raii; using ECommons.ExcelServices; @@ -11,6 +10,7 @@ using Questionable.Model.Common; using Questionable.Model.Questing; using Quest = Questionable.Domain.Quest; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows.ConfigComponents; @@ -256,7 +256,7 @@ public override void DrawTab() using (ImRaii.PushIndent(ImGui.GetFrameHeight() + ImGui.GetStyle().ItemInnerSpacing.X)) { - using (_ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed)) + using (_ = ImRaii.PushColor(ImGuiCol.Text, QstTheme.Danger)) { ImGui.TextUnformatted(_L("Work in Progress:")); ImGui.BulletText(_L("Will always use BossMod for combat (ignoring the configured combat module).")); @@ -625,7 +625,7 @@ private void DrawQuestTable(string label, IReadOnlyList du if (!dutyInfo.Enabled) { ImGuiComponents.HelpMarker(_L("Questionable doesn't include support for this quest yet."), - FontAwesomeIcon.Times, ImGuiColors.DalamudRed); + FontAwesomeIcon.Times, QstTheme.Danger); } else if (dutyInfo.Notes.Count > 0) DrawNotes(dutyInfo.EnabledByDefault, dutyInfo.Notes); diff --git a/Questionable/Windows/JournalComponents/AlliedSocietyJournalComponent.cs b/Questionable/Windows/JournalComponents/AlliedSocietyJournalComponent.cs index 843175c2a..8745dd07a 100644 --- a/Questionable/Windows/JournalComponents/AlliedSocietyJournalComponent.cs +++ b/Questionable/Windows/JournalComponents/AlliedSocietyJournalComponent.cs @@ -1,10 +1,10 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Utility.Raii; using FFXIVClientStructs.FFXIV.Client.Game; using Questionable.Model.Common; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows.JournalComponents; internal sealed class AlliedSocietyJournalComponent @@ -39,7 +39,7 @@ public void DrawAlliedSocietyQuests() bool abandonQuestBeforeCompletion = configuration.Advanced.AbandonQuestBeforeCompletion; bool removeFromPriorityWhenAbandoned = configuration.Advanced.RemoveFromPriorityWhenAbandoned; bool runCommandAfterStop = configuration.Stop.RunCommandAfterStop; - if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.Stop, preventQuestCompletion ? ImGuiColors.DalamudOrange : null)) + if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.Stop, preventQuestCompletion ? QstTheme.Accent : null)) { configuration.Advanced.PreventQuestCompletion = !preventQuestCompletion; pluginInterface.SavePluginConfig(configuration); @@ -48,7 +48,7 @@ public void DrawAlliedSocietyQuests() ImGui.SetTooltip(_L("Prevent quest completion")); ImGui.SameLine(); - if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.Ban, abandonQuestBeforeCompletion ? ImGuiColors.DalamudOrange : null)) + if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.Ban, abandonQuestBeforeCompletion ? QstTheme.Accent : null)) { configuration.Advanced.AbandonQuestBeforeCompletion = !abandonQuestBeforeCompletion; pluginInterface.SavePluginConfig(configuration); @@ -57,7 +57,7 @@ public void DrawAlliedSocietyQuests() ImGui.SetTooltip(_L("Abandon quest before completion")); ImGui.SameLine(); - if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.Trash, removeFromPriorityWhenAbandoned ? ImGuiColors.DalamudOrange : null)) + if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.Trash, removeFromPriorityWhenAbandoned ? QstTheme.Accent : null)) { configuration.Advanced.RemoveFromPriorityWhenAbandoned = !removeFromPriorityWhenAbandoned; pluginInterface.SavePluginConfig(configuration); @@ -66,7 +66,7 @@ public void DrawAlliedSocietyQuests() ImGui.SetTooltip(_L("Remove from priority when abandoned")); ImGui.SameLine(); - if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.Home, runCommandAfterStop ? ImGuiColors.DalamudOrange : null)) + if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.Home, runCommandAfterStop ? QstTheme.Accent : null)) { configuration.Stop.RunCommandAfterStop = !runCommandAfterStop; pluginInterface.SavePluginConfig(configuration); @@ -86,14 +86,14 @@ public void DrawAlliedSocietyQuests() { ImGui.SameLine(); ImGuiComponents.HelpMarker(_LF("Quests marked with yellow have not been completed once yet on this character."), - FontAwesomeIcon.InfoCircle, ImGuiColors.DalamudYellow); + FontAwesomeIcon.InfoCircle, QstTheme.Amber); } if (_unchecked > 0) { ImGui.SameLine(); ImGuiComponents.HelpMarker(_L("Quests marked with orange need to be reported as working or not via the LastChecked system. Ask Aly for more details!"), - FontAwesomeIcon.InfoCircle, ImGuiColors.DalamudOrange); + FontAwesomeIcon.InfoCircle, QstTheme.Accent); ImGui.SameLine(); ImGui.Text(_LF("Unchecked: {0}", _unchecked)); } @@ -125,7 +125,7 @@ public void DrawAlliedSocietyQuests() ) )) { - using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange)) // highlight the category orange + using (ImRaii.PushColor(ImGuiCol.Text, QstTheme.Accent)) // highlight the category orange { isOpen = ImGui.CollapsingHeader(label); } @@ -133,7 +133,7 @@ public void DrawAlliedSocietyQuests() } else if (quests.Any(x => !questFunctions.IsQuestComplete(x.QuestId))) // if the character has not completed a quest in this category { - using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow)) + using (ImRaii.PushColor(ImGuiCol.Text, QstTheme.Amber)) { isOpen = ImGui.CollapsingHeader(label); } @@ -177,20 +177,20 @@ private void DrawQuest(QuestInfo questInfo, bool addPending = false, bool showRe bool fate = false; string lastChecked = ""; if (!questRegistry.TryGetQuest(questInfo.QuestId, out Quest? quest)) - color = ImGuiColors.DalamudGrey; + color = QstTheme.TextMuted; else { if (quest.Root.LastChecked.Date != null) { lastChecked = $"({quest.Root.LastChecked.Date})"; if (quest.Root.LastChecked.Since(DateTime.Now)!.Value.TotalDays > 30) - color = ImGuiColors.DalamudOrange; + color = QstTheme.Accent; } else - color = ImGuiColors.ParsedOrange; + color = QstTheme.Accent; if (quest.Root.Disabled && (quest.Root.Comment ?? "").Contains("FATE")) { - color = ImGuiColors.DalamudRed; + color = QstTheme.Danger; fate = true; } } @@ -202,7 +202,7 @@ private void DrawQuest(QuestInfo questInfo, bool addPending = false, bool showRe checklistItem = $"[+{questInfo.SocietyRepValue}] " + checklistItem; if (uiUtils.ChecklistItem(checklistItem, color, icon)) questTooltipComponent.Draw(questInfo); - if (addPending && (color.Equals(ImGuiColors.DalamudOrange) || color.Equals(ImGuiColors.ParsedOrange))) + if (addPending && (color.Equals(QstTheme.Accent) || color.Equals(QstTheme.Accent))) questController.PriorityManager.Add(questInfo.QuestId); questJournalUtils.ShowContextMenu(questInfo, quest, nameof(AlliedSocietyJournalComponent)); @@ -211,7 +211,7 @@ private void DrawQuest(QuestInfo questInfo, bool addPending = false, bool showRe { ImGui.SameLine(); using (pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) - ImGui.TextColored(ImGuiColors.DalamudYellow, FontAwesomeIcon.ExclamationCircle.ToIconString()); + ImGui.TextColored(QstTheme.Amber, FontAwesomeIcon.ExclamationCircle.ToIconString()); if (ImGui.IsItemHovered()) ImGui.SetTooltip(_L("This quest is in Priority Quests.")); } diff --git a/Questionable/Windows/JournalComponents/GatheringJournalComponent.cs b/Questionable/Windows/JournalComponents/GatheringJournalComponent.cs index ea538dc62..4ecc862f5 100644 --- a/Questionable/Windows/JournalComponents/GatheringJournalComponent.cs +++ b/Questionable/Windows/JournalComponents/GatheringJournalComponent.cs @@ -1,12 +1,12 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility.Raii; using ECommons.ExcelServices; using FFXIVClientStructs.FFXIV.Client.Game; using Lumina.Excel.Sheets; using Questionable.Model.Gathering; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows.JournalComponents; internal sealed class GatheringJournalComponent @@ -260,7 +260,7 @@ private void DrawItem(ushort item, GatheringPointId pointId) if (item < 10_000) _uiUtils.ChecklistItem(string.Empty, _gatheredItems.Contains(item)); else - _uiUtils.ChecklistItem(string.Empty, ImGuiColors.DalamudGrey, FontAwesomeIcon.Minus); + _uiUtils.ChecklistItem(string.Empty, QstTheme.TextMuted, FontAwesomeIcon.Minus); } private static void DrawCount(int count, int total) @@ -271,7 +271,7 @@ private static void DrawCount(int count, int total) string text = $"{count.ToString(CultureInfo.CurrentCulture).PadLeft(len.Length)} / {total.ToString(CultureInfo.CurrentCulture).PadLeft(len.Length)}"; if (count == total) - ImGui.TextColored(ImGuiColors.ParsedGreen, text); + ImGui.TextColored(QstTheme.Success, text); else ImGui.TextUnformatted(text); diff --git a/Questionable/Windows/JournalComponents/QuestJournalComponent.cs b/Questionable/Windows/JournalComponents/QuestJournalComponent.cs index bdf380953..a6da1ab32 100644 --- a/Questionable/Windows/JournalComponents/QuestJournalComponent.cs +++ b/Questionable/Windows/JournalComponents/QuestJournalComponent.cs @@ -1,8 +1,8 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility.Raii; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows.JournalComponents; internal sealed class QuestJournalComponent @@ -33,7 +33,7 @@ public void DrawQuests() if (!tab) return; - if (ImGui.CollapsingHeader(_L("Explanation"))) + if (QstWidgets.SectionHeader(_L("Explanation"), "JournalExplanation", defaultOpen: false)) { ImGui.Text(_L("The list below contains all quests that appear in your journal.")); ImGui.BulletText(_L("'Supported' lists quests that Questionable can do for you")); @@ -41,7 +41,7 @@ public void DrawQuests() ImGui.BulletText( _L("Not all quests can be completed even if they're listed as available, e.g. starting city quest chains.")); ImGui.BulletText(_L("The text in the Supported column indicates the last time a quest path was reported to work perfectly.")); - ImGui.TextColoredWrapped(ImGuiColors.DalamudYellow, _L("Quests can be added to Priority Quests, either individually or by group, with the right click menu.")); + ImGui.TextColoredWrapped(QstTheme.Amber, _L("Quests can be added to Priority Quests, either individually or by group, with the right click menu.")); ImGui.Spacing(); ImGui.Separator(); @@ -209,7 +209,7 @@ internal void DrawQuest(QuestInfo questInfo) { ImGui.SameLine(); using (pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) - ImGui.TextColored(ImGuiColors.DalamudYellow, FontAwesomeIcon.ExclamationCircle.ToIconString()); + ImGui.TextColored(QstTheme.Amber, FontAwesomeIcon.ExclamationCircle.ToIconString()); if (ImGui.IsItemHovered()) ImGui.SetTooltip(_L("This quest is in Priority Quests.")); } @@ -231,12 +231,12 @@ internal void DrawQuest(QuestInfo questInfo) if (QuestFunctions.IsQuestRemoved(questInfo.QuestId)) { - if (uiUtils.ChecklistItem(lastChecked, ImGuiColors.DalamudGrey, FontAwesomeIcon.Minus)) + if (uiUtils.ChecklistItem(lastChecked, QstTheme.TextMuted, FontAwesomeIcon.Minus)) ImGui.SetTooltip(_L("This quest is not available.") + addendum); } else if (fate) { - if (uiUtils.ChecklistItem(lastChecked, ImGuiColors.DalamudOrange, FontAwesomeIcon.ExclamationTriangle)) + if (uiUtils.ChecklistItem(lastChecked, QstTheme.Accent, FontAwesomeIcon.ExclamationTriangle)) ImGui.SetTooltip(_L("This quest requires completing a FATE.") + addendum); } else if (quest is { Root.Disabled: false }) @@ -244,12 +244,12 @@ internal void DrawQuest(QuestInfo questInfo) List issues = questValidator.GetIssues(quest.Id); if (issues.Any(x => x.Severity == EIssueSeverity.Error)) { - if (uiUtils.ChecklistItem(lastChecked, ImGuiColors.DalamudRed, FontAwesomeIcon.ExclamationTriangle)) + if (uiUtils.ChecklistItem(lastChecked, QstTheme.Danger, FontAwesomeIcon.ExclamationTriangle)) ImGui.SetTooltip(_L("This quest could not be loaded.") + addendum); } else if (issues.Count > 0) { - if (uiUtils.ChecklistItem(lastChecked, ImGuiColors.ParsedBlue, FontAwesomeIcon.InfoCircle)) + if (uiUtils.ChecklistItem(lastChecked, QstTheme.Info, FontAwesomeIcon.InfoCircle)) ImGui.SetTooltip(_L("This quest had validation issues.") + addendum); } else if (uiUtils.ChecklistItem(lastChecked, complete: true)) @@ -266,7 +266,7 @@ internal void DrawQuest(QuestInfo questInfo) { ImGui.SameLine(); using (pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) - ImGui.TextColored(ImGuiColors.DalamudYellow, FontAwesomeIcon.StopCircle.ToIconString()); + ImGui.TextColored(QstTheme.Amber, FontAwesomeIcon.StopCircle.ToIconString()); if (ImGui.IsItemHovered()) ImGui.SetTooltip(_L("This quest is in Stop Conditions.")); } @@ -279,10 +279,10 @@ internal void DrawQuest(QuestInfo questInfo) internal static void DrawCount(int count, int total) { string len = 9999.ToString(CultureInfo.CurrentCulture); - ImGui.PushFont(UiBuilder.MonoFont); + using var monoFont = ImRaii.PushFont(UiBuilder.MonoFont); if (total == 0) - ImGui.TextColored(ImGuiColors.DalamudGrey, $"{" ".PadLeft(len.Length)} - {" ".PadRight(len.Length)}"); + ImGui.TextColored(QstTheme.TextMuted, $"{" ".PadLeft(len.Length)} - {" ".PadRight(len.Length)}"); else if (count == 0) { ImGui.TextUnformatted($"{"-".PadLeft(len.Length)} / {total.ToString(CultureInfo.CurrentCulture).PadRight(len.Length)}?"); @@ -292,12 +292,10 @@ internal static void DrawCount(int count, int total) string text = $"{count.ToString(CultureInfo.CurrentCulture).PadLeft(len.Length)} / {total.ToString(CultureInfo.CurrentCulture).PadRight(len.Length)}"; if (count == total) - ImGui.TextColored(ImGuiColors.ParsedGreen, text); + ImGui.TextColored(QstTheme.Success, text); else ImGui.TextUnformatted(text); } - - ImGui.PopFont(); } public void UpdateFilter() diff --git a/Questionable/Windows/JournalComponents/QuestJournalUtils.cs b/Questionable/Windows/JournalComponents/QuestJournalUtils.cs index eeee550e4..2fa410ddf 100644 --- a/Questionable/Windows/JournalComponents/QuestJournalUtils.cs +++ b/Questionable/Windows/JournalComponents/QuestJournalUtils.cs @@ -18,6 +18,7 @@ internal sealed class QuestJournalUtils AetheryteFunctions aetheryteFunctions, MovementController movementController, IGameGui gameGui, + PathEditorWindow pathEditorWindow, AutoGen.DraftQuestPathService draftQuestPathService) { public void ShowContextMenu(IQuestInfo questInfo, Quest? quest, string label) @@ -137,6 +138,9 @@ public void ShowContextMenu(IQuestInfo questInfo, Quest? quest, string label) using (ImRaii.PushIndent()) { + if (ImGui.MenuItem(_L("Open in Path Editor"))) + pathEditorWindow.Open(questInfo.QuestId); + if (ImGui.MenuItem(_L("Edit quest path"))) (bool success, string filename) = QuestRegistry.OpenEditor(questInfo); diff --git a/Questionable/Windows/JournalComponents/QuestRewardComponent.cs b/Questionable/Windows/JournalComponents/QuestRewardComponent.cs index 32bf995c2..644078f47 100644 --- a/Questionable/Windows/JournalComponents/QuestRewardComponent.cs +++ b/Questionable/Windows/JournalComponents/QuestRewardComponent.cs @@ -1,9 +1,9 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Game.Text; using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility.Raii; using Questionable.Model.Common; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows.JournalComponents; internal sealed class QuestRewardComponent @@ -58,10 +58,10 @@ private void DrawGroup(string label, EItemRewardType type) bool complete = item.IsUnlocked(); Vector4 color = !questRegistry.IsKnownQuest(item.ElementId) - ? ImGuiColors.DalamudGrey + ? QstTheme.TextMuted : complete - ? ImGuiColors.ParsedGreen - : ImGuiColors.DalamudRed; + ? QstTheme.Success + : QstTheme.Danger; FontAwesomeIcon icon = complete ? FontAwesomeIcon.Check : FontAwesomeIcon.Times; if (uiUtils.ChecklistItem(name, color, icon)) { diff --git a/Questionable/Windows/JournalComponents/RedoComponent.cs b/Questionable/Windows/JournalComponents/RedoComponent.cs index 25cb7d6b8..109c35b3f 100644 --- a/Questionable/Windows/JournalComponents/RedoComponent.cs +++ b/Questionable/Windows/JournalComponents/RedoComponent.cs @@ -1,10 +1,10 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Utility.Raii; using Lumina.Excel.Sheets; using Questionable.Model.Questing; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows.JournalComponents; internal sealed class RedoComponent @@ -60,7 +60,7 @@ public void DrawRedoChapters() { ImGui.SameLine(); if (ImGuiComponentsLocal.IconButton(_hideDone ? FontAwesomeIcon.ChevronRight : FontAwesomeIcon.ChevronDown, - _hideDone ? ImGuiColors.DalamudOrange : null)) + _hideDone ? QstTheme.Accent : null)) { _hideDone = !_hideDone; } @@ -69,7 +69,7 @@ public void DrawRedoChapters() } ImGui.SameLine(); ImGuiComponents.HelpMarker(_L("Quests marked with orange need to be reported as working or not via the LastChecked system. Ask Aly for more details!"), - FontAwesomeIcon.InfoCircle, ImGuiColors.DalamudOrange); + FontAwesomeIcon.InfoCircle, QstTheme.Accent); ImGui.SameLine(); ImGui.Text(_L("Active:")); ImGui.SameLine(); @@ -122,7 +122,7 @@ public void DrawRedoChapters() ImGui.TableNextColumn(); ImRaii.ColorDisposable? disposable = null; if (checkQuests.Length > 0) - disposable = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange); + disposable = ImRaii.PushColor(ImGuiCol.Text, QstTheme.Accent); bool open = ImGui.TreeNodeEx($"{chapter.RowId}", ImGuiTreeNodeFlags.SpanFullWidth, $"{categoryName}{chapterName}"); disposable?.Dispose(); if (checkQuests.Length > 0 && checkQuests[0] != null && ImGui.IsItemHovered()) @@ -131,7 +131,7 @@ public void DrawRedoChapters() var index = redoUtil.GetChapter(checkQuests[0]!.Id.Value); ImGui.Text(_LF("({0}) Unchecked: #{1}{2} ({3}/{4})", chapter.RowId, index.SimplifiedIndex, (checkQuests.Length > 1 ? "+" : ""), checkQuests.Length, redoCache.Quests.Count)); - using var __ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange); + using var __ = ImRaii.PushColor(ImGuiCol.Text, QstTheme.Accent); ImGui.Text(string.Join('\n', checkQuests.Select(q => $"{q?.Info.SimplifiedName} ({q?.Id})"))); } @@ -173,7 +173,7 @@ public void DrawRedoChapters() ImGui.TreeNodeEx($"{q.Name} ({(ushort)q.RowId})", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.SpanFullWidth); ImGui.TableNextColumn(); - if (uiUtils.ChecklistItem("", ImGuiColors.DalamudGrey, FontAwesomeIcon.Minus)) + if (uiUtils.ChecklistItem("", QstTheme.TextMuted, FontAwesomeIcon.Minus)) ImGui.SetTooltip(_L("This quest is not supported.")); ImGui.TableNextColumn(); } diff --git a/Questionable/Windows/OneTimeSetupWindow.cs b/Questionable/Windows/OneTimeSetupWindow.cs index 9399c1592..15c5f0a61 100644 --- a/Questionable/Windows/OneTimeSetupWindow.cs +++ b/Questionable/Windows/OneTimeSetupWindow.cs @@ -1,8 +1,8 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility.Raii; using Questionable.Windows.Common; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows; internal sealed class OneTimeSetupWindow : LWindow @@ -43,7 +43,7 @@ public override void DrawContent() if (allRequiredInstalled) { - using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedGreen)) + using (ImRaii.PushColor(ImGuiCol.Text, QstTheme.Success)) { if (ImGuiComponentsLocal.IconButtonWithText(FontAwesomeIcon.Check, _L("Finish Setup"))) { @@ -58,7 +58,7 @@ public override void DrawContent() { using (ImRaii.Disabled()) { - using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed)) + using (ImRaii.PushColor(ImGuiCol.Text, QstTheme.Danger)) { ImGuiComponentsLocal.IconButtonWithText(FontAwesomeIcon.Check, _L("Missing required plugins")); } diff --git a/Questionable/Windows/PathEditorComponents/PathEditorSession.cs b/Questionable/Windows/PathEditorComponents/PathEditorSession.cs new file mode 100644 index 000000000..962a26228 --- /dev/null +++ b/Questionable/Windows/PathEditorComponents/PathEditorSession.cs @@ -0,0 +1,64 @@ +using System.Text.Json; +using Questionable.Model.Questing; + +namespace Questionable.Windows.PathEditorComponents; + +internal sealed class PathEditorSession(QuestRegistry questRegistry, QuestData questData) +{ + public ElementId? QuestId { get; private set; } + public QuestInfo? Info { get; private set; } + public QuestRoot? WorkingRoot { get; private set; } + public int SelectedSequenceIndex { get; set; } + public int SelectedStepIndex { get; set; } + public bool Dirty { get; set; } + + public QuestSequence? SelectedSequence => + WorkingRoot != null && SelectedSequenceIndex >= 0 && SelectedSequenceIndex < WorkingRoot.QuestSequence.Count + ? WorkingRoot.QuestSequence[SelectedSequenceIndex] + : null; + + public QuestStep? SelectedStep => + SelectedSequence is { } sequence && SelectedStepIndex >= 0 && SelectedStepIndex < sequence.Steps.Count + ? sequence.Steps[SelectedStepIndex] + : null; + + public void Load(ElementId questId) + { + QuestId = questId; + try + { + Info = questData.GetQuestInfo(questId) as QuestInfo; + } + catch (Exception) + { + Info = null; + } + + if (questRegistry.TryGetQuest(questId, out Quest? quest)) + { + WorkingRoot = Clone(quest.Root); + Dirty = false; + } + else if (Info != null) + { + WorkingRoot = QuestRegistry.CreateQuestRoot(Info); + Dirty = true; + } + else + WorkingRoot = null; + + SelectedSequenceIndex = 0; + SelectedStepIndex = 0; + } + + public void Discard() + { + if (QuestId != null) + Load(QuestId); + } + + private static QuestRoot Clone(QuestRoot root) + { + return JsonSerializer.SerializeToNode(root, JsonOptions.Default)!.Deserialize()!; + } +} diff --git a/Questionable/Windows/PathEditorComponents/StepCaptureComponent.cs b/Questionable/Windows/PathEditorComponents/StepCaptureComponent.cs new file mode 100644 index 000000000..a7eb3b0b5 --- /dev/null +++ b/Questionable/Windows/PathEditorComponents/StepCaptureComponent.cs @@ -0,0 +1,69 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using Questionable.Model.Questing; +using Questionable.Windows.Common.Ui; + +namespace Questionable.Windows.PathEditorComponents; + +internal sealed class StepCaptureComponent( + ITargetManager targetManager, + IClientState clientState, + IObjectTable objectTable) +{ + public void Draw(PathEditorSession session) + { + QuestSequence? sequence = session.SelectedSequence; + if (!QstWidgets.SectionHeader(_L("Capture"), "PathEditorCapture")) + return; + + IGameObject? target = targetManager.Target; + if (target != null) + { + using (QstWidgets.Card(QstTheme.WithAlpha(QstTheme.Accent, 0.45f))) + { + EInteractionType guessedType = QuestStepCapture.GuessInteractionType(target); + ImGui.TextUnformatted(target.Name.ToString()); + ImGui.SameLine(); + QstWidgets.Chip(GameFunctions.GetBaseID(target).ToString(CultureInfo.InvariantCulture), QstTheme.Info); + ImGui.SameLine(); + if (QstWidgets.Chip(guessedType.ToString(), QstTheme.Accent)) + ImGui.SetTooltip(_L("Interaction type detected from the target's nameplate icon.")); + + using (ImRaii.Disabled(sequence == null)) + { + if (ImGuiComponentsLocal.IconButtonWithText(FontAwesomeIcon.Plus, + _LF("Append step from target to Seq {0}", sequence?.Sequence ?? 0)) + && sequence != null) + { + sequence.Steps.Add(QuestStepCapture.CreateStepFromTarget(target, clientState.TerritoryType)); + session.SelectedStepIndex = sequence.Steps.Count - 1; + session.Dirty = true; + } + } + } + } + else + { + using ImRaii.DisabledDisposable _ = ImRaii.Disabled(); + ImGui.TextUnformatted(_L("No target selected.")); + } + + if (objectTable[0] is { } player) + { + using (ImRaii.Disabled(sequence == null)) + { + if (ImGuiComponentsLocal.IconButtonWithText(FontAwesomeIcon.MapMarkerAlt, + _L("Append step at my position")) + && sequence != null) + { + sequence.Steps.Add(QuestStepCapture.CreatePositionStep(player.Position, clientState.TerritoryType)); + session.SelectedStepIndex = sequence.Steps.Count - 1; + session.Dirty = true; + } + } + } + } +} diff --git a/Questionable/Windows/PathEditorComponents/StepFormComponent.cs b/Questionable/Windows/PathEditorComponents/StepFormComponent.cs new file mode 100644 index 000000000..c57a3bf2c --- /dev/null +++ b/Questionable/Windows/PathEditorComponents/StepFormComponent.cs @@ -0,0 +1,187 @@ +using System.Text.Json; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using Questionable.Model.Common; +using Questionable.Model.Questing; +using Questionable.Windows.Common.Ui; + +namespace Questionable.Windows.PathEditorComponents; + +internal sealed class StepFormComponent +{ + private static readonly EInteractionType[] InteractionTypes = Enum.GetValues(); + private static readonly string[] InteractionTypeLabels = + InteractionTypes.Select(x => x.ToString()).ToArray(); + + private static readonly EAetheryteLocation?[] AetheryteValues = + new EAetheryteLocation?[] { null } + .Concat(Enum.GetValues().Cast()) + .ToArray(); + private static readonly string[] AetheryteLabels = + new[] { "-" }.Concat(Enum.GetValues().Select(x => x.ToString())).ToArray(); + + private string _interactionTypeSearch = string.Empty; + private string _aetheryteSearch = string.Empty; + + public void Draw(PathEditorSession session) + { + QuestStep? step = session.SelectedStep; + if (step == null) + { + using ImRaii.DisabledDisposable _ = ImRaii.Disabled(); + ImGui.TextUnformatted(_L("Select a step to edit.")); + return; + } + + EInteractionType selectedType = step.InteractionType; + ImGuiComponentsLocal.DrawSearchableCombo(_L("Interaction Type"), InteractionTypes, InteractionTypeLabels, + step.InteractionType, ref _interactionTypeSearch, ref selectedType); + if (selectedType != step.InteractionType) + { + step.InteractionType = selectedType; + session.Dirty = true; + } + + float inputWidth = ImGui.GetWindowContentRegionMax().X / 2; + + string dataId = step.DataId?.ToString(CultureInfo.InvariantCulture) ?? string.Empty; + ImGui.SetNextItemWidth(inputWidth); + if (ImGui.InputText(_L("Data ID"), ref dataId, 12, ImGuiInputTextFlags.CharsDecimal)) + { + step.DataId = uint.TryParse(dataId, out uint parsedDataId) ? parsedDataId : null; + session.Dirty = true; + } + + bool hasPosition = step.Position != null; + if (ImGui.Checkbox($"{_L("Position")}###HasPosition", ref hasPosition)) + { + step.Position = hasPosition ? Vector3.Zero : null; + session.Dirty = true; + } + + if (step.Position is { } position) + { + ImGui.SameLine(); + ImGui.SetNextItemWidth(inputWidth); + Vector3 editablePosition = position; + if (ImGui.InputFloat3("###PositionValue", ref editablePosition)) + { + step.Position = editablePosition; + session.Dirty = true; + } + } + + string territoryId = step.TerritoryId.ToString(CultureInfo.InvariantCulture); + ImGui.SetNextItemWidth(inputWidth); + if (ImGui.InputText(_L("Territory ID"), ref territoryId, 8, ImGuiInputTextFlags.CharsDecimal) + && uint.TryParse(territoryId, out uint parsedTerritoryId)) + { + step.TerritoryId = parsedTerritoryId; + session.Dirty = true; + } + + float stopDistance = step.StopDistance ?? 0f; + ImGui.SetNextItemWidth(inputWidth); + if (ImGui.InputFloat(_L("Stop Distance"), ref stopDistance, 0.1f, 1f, "%.2f")) + { + step.StopDistance = stopDistance > 0f ? stopDistance : null; + session.Dirty = true; + } + + EAetheryteLocation? selectedAetheryte = step.AetheryteShortcut; + ImGuiComponentsLocal.DrawSearchableCombo(_L("Aetheryte Shortcut"), AetheryteValues, AetheryteLabels, + step.AetheryteShortcut, ref _aetheryteSearch, ref selectedAetheryte); + if (selectedAetheryte != step.AetheryteShortcut) + { + step.AetheryteShortcut = selectedAetheryte; + session.Dirty = true; + } + + string comment = step.Comment ?? string.Empty; + ImGui.SetNextItemWidth(inputWidth); + if (ImGui.InputTextWithHint(_L("Comment"), _L("Shown in the debug overlay"), ref comment, 256)) + { + step.Comment = string.IsNullOrWhiteSpace(comment) ? null : comment; + session.Dirty = true; + } + + bool fly = step.Fly == true; + if (ImGui.Checkbox(_L("Fly"), ref fly)) + { + step.Fly = fly ? true : null; + session.Dirty = true; + } + + DrawValidationHints(step); + DrawStepActions(session); + DrawJsonPreview(step); + } + + private static void DrawValidationHints(QuestStep step) + { + if (step.DataId == null + && step.InteractionType is EInteractionType.Interact or EInteractionType.AcceptQuest + or EInteractionType.CompleteQuest or EInteractionType.AttuneAetheryte) + Hint(QstTheme.Amber, _L("This interaction type usually needs a Data ID.")); + + if (step.Position == null + && step.InteractionType is EInteractionType.WalkTo or EInteractionType.Jump or EInteractionType.Dive) + Hint(QstTheme.Amber, _L("This interaction type usually needs a Position.")); + + if (step.TerritoryId == 0) + Hint(QstTheme.Danger, _L("Territory ID is not set.")); + } + + private static void Hint(Vector4 color, string text) + { + ImGui.TextColored(color, text); + } + + private static void DrawStepActions(PathEditorSession session) + { + QuestSequence? sequence = session.SelectedSequence; + QuestStep? step = session.SelectedStep; + if (sequence == null || step == null) + return; + + ImGui.Spacing(); + if (ImGuiComponentsLocal.IconButtonWithText(FontAwesomeIcon.Copy, _L("Duplicate"))) + { + sequence.Steps.Insert(session.SelectedStepIndex + 1, CloneStep(step)); + session.SelectedStepIndex++; + session.Dirty = true; + } + + ImGui.SameLine(); + if (ImGuiComponentsLocal.IconButtonWithText(FontAwesomeIcon.Trash, _L("Delete Step"), QstTheme.Danger)) + { + sequence.Steps.RemoveAt(session.SelectedStepIndex); + session.SelectedStepIndex = Math.Max(0, session.SelectedStepIndex - 1); + session.Dirty = true; + } + } + + private static void DrawJsonPreview(QuestStep step) + { + if (!QstWidgets.SectionHeader(_L("Step JSON"), "StepJson")) + return; + + string json = JsonSerializer.Serialize(step, JsonOptions.Default); + if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.Copy)) + ImGui.SetClipboardText(json); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(_L("Copy this step as JSON")); + + using ImRaii.ColorDisposable textColor = ImRaii.PushColor(ImGuiCol.Text, QstTheme.TextMuted); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImGui.TextUnformatted(json); + } + } + + private static QuestStep CloneStep(QuestStep step) + { + return JsonSerializer.SerializeToNode(step, JsonOptions.Default)!.Deserialize()!; + } +} diff --git a/Questionable/Windows/PathEditorWindow.cs b/Questionable/Windows/PathEditorWindow.cs new file mode 100644 index 000000000..13dc5932c --- /dev/null +++ b/Questionable/Windows/PathEditorWindow.cs @@ -0,0 +1,290 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Questionable.Model.Questing; +using Questionable.Validation; +using Questionable.Windows.Common; +using Questionable.Windows.Common.Ui; +using Questionable.Windows.PathEditorComponents; + +namespace Questionable.Windows; + +internal sealed class PathEditorWindow : LWindow +{ + private readonly PathEditorSession _session; + private readonly StepFormComponent _stepFormComponent; + private readonly StepCaptureComponent _stepCaptureComponent; + private readonly QuestController _questController; + private readonly QuestValidator _questValidator; + private readonly QuestSelector _questSelector; + private readonly IChatGui _chatGui; + + private ElementId? _pendingQuestId; + + public PathEditorWindow(PathEditorSession session, StepFormComponent stepFormComponent, + StepCaptureComponent stepCaptureComponent, QuestController questController, QuestValidator questValidator, + QuestSelector questSelector, IChatGui chatGui) + : base(_L("Path Editor") + "###QuestionablePathEditor") + { + _session = session; + _stepFormComponent = stepFormComponent; + _stepCaptureComponent = stepCaptureComponent; + _questController = questController; + _questValidator = questValidator; + _questSelector = questSelector; + _chatGui = chatGui; + + Size = new Vector2(760, 480); + SizeCondition = ImGuiCond.Once; + SizeConstraints = new WindowSizeConstraints + { + MinimumSize = new Vector2(640, 360) + }; + + _questSelector.QuestSelected = quest => + { + if (_session.Dirty && _session.QuestId != null && _session.QuestId != quest.Id) + _pendingQuestId = quest.Id; + else + _session.Load(quest.Id); + }; + } + + public void Open(ElementId questId) + { + if (_session.QuestId != questId) + _session.Load(questId); + IsOpenAndUncollapsed = true; + BringToFront(); + } + + public override void DrawContent() + { + DrawToolbar(); + DrawUnsavedChangesGuard(); + + using (ImRaii.ChildDisposable tree = ImRaii.Child("PathEditorTree", + new Vector2(240 * ImGuiHelpers.GlobalScale, 0), border: true)) + { + if (tree) + DrawSequenceTree(); + } + + ImGui.SameLine(); + + using ImRaii.ChildDisposable detail = ImRaii.Child("PathEditorDetail", Vector2.Zero, border: false); + if (detail) + { + _stepCaptureComponent.Draw(_session); + if (QstWidgets.SectionHeader(_L("Step"), "PathEditorStep")) + _stepFormComponent.Draw(_session); + DrawValidationFooter(); + } + } + + private void DrawToolbar() + { + _questSelector.DrawSelection(); + + if (_session.Info is { } info) + { + ImGui.TextUnformatted(QuestRegistry.GetFilename(info)); + if (_session.Dirty) + { + ImGui.SameLine(); + QstWidgets.Chip(_L("unsaved"), QstTheme.Accent); + } + } + else + { + using ImRaii.DisabledDisposable _ = ImRaii.Disabled(); + ImGui.TextUnformatted(_L("Select a quest to edit.")); + } + + using (ImRaii.Disabled(!_session.Dirty || _session.Info == null || _session.WorkingRoot == null)) + { + if (ImGuiComponentsLocal.IconButtonWithText(FontAwesomeIcon.Save, _L("Save"), QstTheme.Success)) + Save(); + } + + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip(_L("Writes the path to your user quest directory and reloads all quest data.\nSaving reformats the file (property order, omitted defaults).")); + + ImGui.SameLine(); + using (ImRaii.Disabled(!_session.Dirty)) + { + if (ImGuiComponentsLocal.IconButtonWithText(FontAwesomeIcon.Undo, _L("Discard"))) + _session.Discard(); + } + + ImGui.SameLine(); + if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.FolderOpen)) + QuestRegistry.OpenFolder(); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(_L("Open Quests folder")); + + if (_session.Info is { } editableInfo) + { + ImGui.SameLine(); + if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.ExternalLinkAlt)) + QuestRegistry.OpenEditor(editableInfo); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(_L("Open this quest in your default .json text editor")); + } + } + + private void DrawUnsavedChangesGuard() + { + if (_pendingQuestId is not { } pendingQuestId) + return; + + ImGui.TextColored(QstTheme.Accent, _L("You have unsaved changes.")); + ImGui.SameLine(); + if (ImGui.SmallButton(_L("Save first"))) + { + Save(); + _session.Load(pendingQuestId); + _pendingQuestId = null; + } + + ImGui.SameLine(); + if (ImGui.SmallButton(_L("Discard changes"))) + { + _session.Load(pendingQuestId); + _pendingQuestId = null; + } + + ImGui.SameLine(); + if (ImGui.SmallButton(_L("Cancel"))) + _pendingQuestId = null; + } + + private void DrawSequenceTree() + { + QuestRoot? root = _session.WorkingRoot; + if (root == null) + { + using ImRaii.DisabledDisposable _ = ImRaii.Disabled(); + ImGui.TextWrapped(_L("Select a quest above, or open the editor from the Journal's right-click menu.")); + return; + } + + for (int sequenceIndex = 0; sequenceIndex < root.QuestSequence.Count; sequenceIndex++) + { + QuestSequence sequence = root.QuestSequence[sequenceIndex]; + using ImRaii.IdDisposable id = ImRaii.PushId(sequenceIndex); + using ImRaii.TreeNodeDisposable node = ImRaii.TreeNode( + _LF("Sequence {0}", sequence.Sequence) + $"###Sequence{sequenceIndex}", + ImGuiTreeNodeFlags.DefaultOpen); + if (!node) + continue; + + for (int stepIndex = 0; stepIndex < sequence.Steps.Count; stepIndex++) + { + QuestStep step = sequence.Steps[stepIndex]; + bool selected = _session.SelectedSequenceIndex == sequenceIndex + && _session.SelectedStepIndex == stepIndex; + if (ImGui.Selectable($"{stepIndex}: {step.InteractionType}###Step{stepIndex}", selected)) + { + _session.SelectedSequenceIndex = sequenceIndex; + _session.SelectedStepIndex = stepIndex; + } + } + + if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.Plus)) + { + uint territoryId = sequence.Steps.LastOrDefault()?.TerritoryId + ?? _session.Info?.IssuerLocation.Territory.RowId ?? 0; + sequence.Steps.Add(new QuestStep(EInteractionType.Interact, null, null, territoryId)); + _session.SelectedSequenceIndex = sequenceIndex; + _session.SelectedStepIndex = sequence.Steps.Count - 1; + _session.Dirty = true; + } + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(_L("Add a step to this sequence")); + + if (sequence.Steps.Count == 0) + { + ImGui.SameLine(); + if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.Trash, QstTheme.Danger)) + { + root.QuestSequence.RemoveAt(sequenceIndex); + _session.SelectedSequenceIndex = 0; + _session.SelectedStepIndex = 0; + _session.Dirty = true; + return; + } + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(_L("Remove this empty sequence")); + } + } + + ImGui.Spacing(); + if (ImGuiComponentsLocal.IconButtonWithText(FontAwesomeIcon.Plus, _L("Add Sequence"))) + { + byte nextSequence = root.QuestSequence + .Select(x => x.Sequence) + .Where(x => x < 255) + .DefaultIfEmpty((byte)0) + .Max(); + QuestSequence newSequence = new() + { + Sequence = (byte)Math.Min(nextSequence + 1, 254), + Steps = [] + }; + int insertIndex = root.QuestSequence.FindIndex(x => x.Sequence == 255); + if (insertIndex < 0) + insertIndex = root.QuestSequence.Count; + root.QuestSequence.Insert(insertIndex, newSequence); + _session.Dirty = true; + } + } + + private void DrawValidationFooter() + { + if (_session.QuestId == null) + return; + + List issues = _questValidator.GetIssues(_session.QuestId); + if (issues.Count == 0) + return; + + if (!QstWidgets.SectionHeader(_L("Validation"), "PathEditorValidation", count: issues.Count)) + return; + + foreach (ValidationIssue issue in issues) + { + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + if (issue.Severity == EIssueSeverity.Error) + ImGui.TextColored(QstTheme.Danger, FontAwesomeIcon.ExclamationTriangle.ToIconString()); + else + ImGui.TextColored(QstTheme.Info, FontAwesomeIcon.InfoCircle.ToIconString()); + } + + ImGui.SameLine(); + ImGui.TextWrapped($"{issue.Sequence?.ToString(CultureInfo.InvariantCulture) ?? "-"} / " + + $"{issue.Step?.ToString(CultureInfo.InvariantCulture) ?? "-"}: {issue.Description}"); + } + } + + private void Save() + { + if (_session.Info == null || _session.WorkingRoot == null || _session.QuestId is not { } questId) + return; + + (bool success, string message) = QuestRegistry.SaveUserPath(_session.Info, _session.WorkingRoot); + if (success) + { + _chatGui.Print(_LF("Saved quest path to '{0}'", message), CommandHandler.MessageTag, + CommandHandler.TagColor); + _questController.Reload(); + _session.Load(questId); + } + else + _chatGui.PrintError(message, CommandHandler.MessageTag, CommandHandler.TagColor); + } +} diff --git a/Questionable/Windows/PriorityWindow.cs b/Questionable/Windows/PriorityWindow.cs index 2ae8c5701..91d6de3a9 100644 --- a/Questionable/Windows/PriorityWindow.cs +++ b/Questionable/Windows/PriorityWindow.cs @@ -1,12 +1,12 @@ -using System.Text; +using System.Text; using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility.Raii; using ECommons.ExcelServices; using FFXIVClientStructs.FFXIV.Client.Game.UI; using Questionable.Model.Questing; using Questionable.Windows.Common; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows; internal sealed class PriorityWindow : LWindow @@ -74,7 +74,7 @@ public override unsafe void DrawContent() LoadPreset(JobQuestsPresetName); _lastKnownJob = currentJob; - if (ImGui.CollapsingHeader(_L("Explanation"))) + if (QstWidgets.SectionHeader(_L("Explanation"), "PriorityExplanation", defaultOpen: false)) { ImGui.TextWrapped( _L("Questionable will generally try to do:")); @@ -221,7 +221,7 @@ private void DrawQuestList() int oldIndex = priorityQuests.IndexOf(draggedItem); (Vector2 topLeft, Vector2 bottomRight) = itemPositions[oldIndex]; - ImGui.GetWindowDrawList().AddRect(topLeft, bottomRight, ImGui.GetColorU32(ImGuiColors.DalamudGrey), 3f, + ImGui.GetWindowDrawList().AddRect(topLeft, bottomRight, ImGui.GetColorU32(QstTheme.TextMuted), 3f, ImDrawFlags.RoundCornersAll); int newIndex = itemPositions.FindIndex(x => ImGui.IsMouseHoveringRect(x.TopLeft, x.BottomRight, clip: true)); @@ -296,7 +296,7 @@ private void ExportToClipboard() private void DrawPresets() { - if (!ImGui.CollapsingHeader(_L("Presets"))) + if (!QstWidgets.SectionHeader(_L("Presets"), "Presets", defaultOpen: false)) return; Dictionary> builtInPresets = GetOrCreateBuiltInPresets(); @@ -333,7 +333,7 @@ private void DrawPresets() ImGui.EndCombo(); } - ImGui.TextColoredWrapped(ImGuiColors.DalamudRed, _L("Selecting a preset will override your current priority list and activate the preset. " + + ImGui.TextColoredWrapped(QstTheme.Danger, _L("Selecting a preset will override your current priority list and activate the preset. " + "You can save your current list as a preset by entering a name below and selecting Save.")); ImGui.Spacing(); @@ -374,9 +374,9 @@ private void DrawPresets() } if (nameIsBuiltIn) - ImGui.TextColored(ImGuiColors.DalamudRed, _L("Cannot overwrite a built-in preset.")); + ImGui.TextColored(QstTheme.Danger, _L("Cannot overwrite a built-in preset.")); else if (nameExists) - ImGui.TextColored(ImGuiColors.DalamudYellow, _L("Hold CTRL to overwrite existing preset.")); + ImGui.TextColored(QstTheme.Amber, _L("Hold CTRL to overwrite existing preset.")); } //TODO Add all jobs for all role quests diff --git a/Questionable/Windows/QuestComponents/ARealmRebornComponent.cs b/Questionable/Windows/QuestComponents/ARealmRebornComponent.cs index 4281fd2b5..beecd46c9 100644 --- a/Questionable/Windows/QuestComponents/ARealmRebornComponent.cs +++ b/Questionable/Windows/QuestComponents/ARealmRebornComponent.cs @@ -1,9 +1,9 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility.Raii; using FFXIVClientStructs.FFXIV.Client.Game.UI; using Questionable.Model.Questing; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows.QuestComponents; internal sealed class ARealmRebornComponent @@ -57,7 +57,7 @@ private void DrawPrimals() ImGui.SetTooltip(_L("Add to priority quests")); ImGui.SameLine(); bool hover = uiUtils.ChecklistItem(_L("Hard Mode Primals"), complete, - configuration.Advanced.SkipARealmRebornHardModePrimals ? ImGuiColors.DalamudGrey : null); + configuration.Advanced.SkipARealmRebornHardModePrimals ? QstTheme.TextMuted : null); if (complete || !hover) return; @@ -83,7 +83,7 @@ private void DrawAllianceRaids() ImGui.SetTooltip(_L("Add to priority quests")); ImGui.SameLine(); bool hover = uiUtils.ChecklistItem(_L("Crystal Tower Raids"), complete, - configuration.Advanced.SkipCrystalTowerRaids ? ImGuiColors.DalamudGrey : null); + configuration.Advanced.SkipCrystalTowerRaids ? QstTheme.TextMuted : null); if (complete || !hover) return; diff --git a/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs b/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs index 5ba22efb5..aa4b037f4 100644 --- a/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs +++ b/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs @@ -1,14 +1,14 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using Dalamud.Bindings.ImGui; using Dalamud.Game.Text; using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility.Raii; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.UI; using Questionable.Controller.Steps.Shared; using Questionable.Model.Common; using Questionable.Model.Questing; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows.QuestComponents; // TODO: refactor — heavy nesting (43 lines indented ≥6 levels, max indent ~13 levels). @@ -23,6 +23,7 @@ internal sealed partial class ActiveQuestComponent Configuration configuration, QuestRegistry questRegistry, PriorityWindow priorityWindow, + PathEditorWindow pathEditorWindow, UiUtils uiUtils, IChatGui chatGui, PathDataUpdater pathDataUpdater, @@ -36,6 +37,7 @@ internal sealed partial class ActiveQuestComponent private readonly ILogger _logger = logger; private readonly MovementController _movementController = movementController; private readonly PriorityWindow _priorityWindow = priorityWindow; + private readonly PathEditorWindow _pathEditorWindow = pathEditorWindow; private readonly QuestController _questController = questController; private readonly QuestFunctions _questFunctions = questFunctions; @@ -53,7 +55,7 @@ public void Draw(bool isMinimized) QuestController.ECurrentQuestType? currentQuestType = currentQuestDetails?.Type; if (pathDataUpdater.WaitingForPluginUpdate) { - using ImRaii.ColorDisposable _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange); + using ImRaii.ColorDisposable _ = ImRaii.PushColor(ImGuiCol.Text, QstTheme.Accent); ImGui.Text(_L("New version available!")); if (ImGui.IsItemHovered()) ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); @@ -63,13 +65,16 @@ public void Draw(bool isMinimized) if (currentQuest != null) { DrawQuestNames(currentQuest, currentQuestType); + if (!isMinimized) + QstWidgets.ThinProgressBar(CalculateQuestProgress(currentQuest), + _questController.IsRunning ? QstTheme.Success : QstTheme.Amber); QuestProgressInfo? questWork = DrawQuestWork(currentQuest, isMinimized); if (_combatController.IsRunning) - ImGui.TextColored(ImGuiColors.DalamudOrange, _L("In Combat")); + ImGui.TextColored(QstTheme.Accent, _L("In Combat")); else if (_questController.CurrentTaskState is { } currentTaskState) { - using ImRaii.ColorDisposable _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange); + using ImRaii.ColorDisposable _ = ImRaii.PushColor(ImGuiCol.Text, QstTheme.Accent); ImGui.TextUnformatted(currentTaskState); } else @@ -84,24 +89,29 @@ public void Draw(bool isMinimized) QuestStep? currentStep = currentSequence?.FindStep(currentQuest.Step); if (!isMinimized) { - using (ImRaii.ColorDisposable color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange, currentStep is { InteractionType: EInteractionType.Instruction or EInteractionType.WaitForManualProgress or EInteractionType.Snipe })) + string comment = currentStep?.Comment ?? + currentSequence?.Comment ?? + currentQuest.Quest.Root.Comment ?? string.Empty; + if (!string.IsNullOrWhiteSpace(comment)) { - ImGui.TextUnformatted(currentStep?.Comment ?? - currentSequence?.Comment ?? - currentQuest.Quest.Root.Comment ?? string.Empty); + bool manualStep = currentStep is { InteractionType: EInteractionType.Instruction or EInteractionType.WaitForManualProgress or EInteractionType.Snipe }; + using ImRaii.ColorDisposable color = ImRaii.PushColor(ImGuiCol.Text, manualStep ? QstTheme.Accent : QstTheme.TextMuted); + ImGui.TextUnformatted(comment); } - //var nextStep = _questController.GetNextStep(); - //ImGui.BeginDisabled(nextStep.Step == null); - ImGui.Text(_questController.ToStatString()); - //ImGui.EndDisabled(); + string stats = _questController.ToStatString(); + if (!string.IsNullOrWhiteSpace(stats)) + { + using ImRaii.DisabledDisposable _ = ImRaii.Disabled(); + ImGui.Text(stats); + } } DrawQuestButtons(currentQuest, currentStep, questWork, isMinimized); } catch (Exception e) { - ImGui.TextColored(ImGuiColors.DalamudRed, e.ToString()); + ImGui.TextColored(QstTheme.Danger, e.ToString()); _logger.LogError(e, "Could not handle active quest buttons"); } @@ -113,17 +123,14 @@ public void Draw(bool isMinimized) bool editButtonLeft = ImGuiComponentsLocal.IconButton(FontAwesomeIcon.Edit); bool editButtonRight = ImGui.IsItemClicked(ImGuiMouseButton.Right); if (editButtonLeft) - { - (bool success, string filename) = currentQuest != null ? QuestRegistry.OpenEditor(currentQuest.Quest.Info) : _questRegistry.OpenEditor(); - _logger.LogDebug("OpenEditor {Success}: {Filename}", success, filename); - } + _pathEditorWindow.Open(currentQuest.Quest.Id); else if (editButtonRight) { QuestRegistry.OpenFolder(); _logger.LogDebug("OpenFolder executed"); } if (ImGui.IsItemHovered()) - ImGui.SetTooltip(QuestRegistry.OpenEditorDescription); + ImGui.SetTooltip(_L("Left click: Open in Path Editor\nRight click: Open Quests folder")); ImGui.SameLine(); if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.Ban) && currentQuest != null) { @@ -142,9 +149,9 @@ public void Draw(bool isMinimized) ImGui.Text(_L("No active quest")); if (!isMinimized) { - var color = ImGuiColors.DalamudGrey; + var color = QstTheme.TextMuted; if (_questRegistry.Count < 500) - color = ImGuiColors.DalamudRed; + color = QstTheme.Danger; ImGui.TextColored(color, _LF("{0} quests loaded", _questRegistry.Count)); if (ImGui.IsItemClicked()) { @@ -170,7 +177,7 @@ public void Draw(bool isMinimized) #if REPORTING if (!_configuration.General.ReportsDisabled) { - Vector4? reportButtonColor = _configuration.General.DismissedReportWarning ? null : ImGuiColors.DalamudRed; + Vector4? reportButtonColor = _configuration.General.DismissedReportWarning ? null : QstTheme.Danger; ImGui.SameLine(); if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.ExclamationCircle, reportButtonColor)) { @@ -188,7 +195,7 @@ private void DrawQuestNames(QuestController.QuestProgress currentQuest, { if (currentQuestType == QuestController.ECurrentQuestType.Simulated) { - using ImRaii.ColorDisposable _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + using ImRaii.ColorDisposable _ = ImRaii.PushColor(ImGuiCol.Text, QstTheme.Danger); ImGui.TextUnformatted(_L("Simulated Quest: ") + _LF("{0} ({1}) / {2} / {3}", Shorten(currentQuest.Quest.Info.Name), @@ -198,7 +205,7 @@ private void DrawQuestNames(QuestController.QuestProgress currentQuest, } else if (currentQuestType == QuestController.ECurrentQuestType.Gathering) { - using ImRaii.ColorDisposable _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedGold); + using ImRaii.ColorDisposable _ = ImRaii.PushColor(ImGuiCol.Text, QstTheme.Amber); ImGui.TextUnformatted(_LF("Gathering: ") + _LF("{0} ({1}) / {2} / {3}", Shorten(currentQuest.Quest.Info.Name), @@ -213,9 +220,11 @@ private void DrawQuestNames(QuestController.QuestProgress currentQuest, { if (startedQuest.Quest.Source == Quest.ESource.UserDirectory) { - ImGui.PushFont(UiBuilder.IconFont); - ImGui.TextColored(ImGuiColors.DalamudRed, FontAwesomeIcon.FilePen.ToIconString()); - ImGui.PopFont(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGui.TextColored(QstTheme.Danger, FontAwesomeIcon.FilePen.ToIconString()); + } + ImGui.SameLine(0); if (ImGui.IsItemHovered()) @@ -225,17 +234,14 @@ private void DrawQuestNames(QuestController.QuestProgress currentQuest, } } - ImGui.TextUnformatted(_L($"Quest: ") + - _LF("{0} ({1}) / {2} / {3}", - Shorten(currentQuest.Quest.Info.Name), - currentQuest.Quest.Id, - currentQuest.Sequence, - currentQuest.Step)); + ImGui.TextUnformatted(_L("Quest: ") + Shorten(currentQuest.Quest.Info.Name)); + ImGui.SameLine(); + QstWidgets.Chip($"#{currentQuest.Quest.Id}", QstTheme.Info); if (startedQuest.Quest.Root.Disabled) { ImGui.SameLine(); - ImGui.TextColored(ImGuiColors.DalamudRed, _L("Disabled")); + ImGui.TextColored(QstTheme.Danger, _L("Disabled")); } bool hasLevelCondition = _configuration.Stop.Enabled && _configuration.Stop.LevelToStopAfter; @@ -248,12 +254,32 @@ private void DrawQuestNames(QuestController.QuestProgress currentQuest, !_questFunctions.IsQuestAcceptedOrComplete(x) && !_questFunctions.IsQuestUnobtainable(x)); - if (hasLevelCondition || hasCompleteQuestConditions || hasAcceptQuestConditions) + List priorityQuests = _questFunctions.NextPriorityQuestsThatCanBeAccepted; + bool anyAvailable = false; + bool anyUnavailable = false; + foreach (var p in priorityQuests) { - ImGui.SameLine(); + if (p.IsAvailable) anyAvailable = true; + else anyUnavailable = true; + if (anyAvailable && anyUnavailable) break; + } + + bool showStopClock = hasLevelCondition || hasCompleteQuestConditions || hasAcceptQuestConditions; + bool showPriorityCrystal = anyAvailable || anyUnavailable; + if (showStopClock || showPriorityCrystal) + { + float clockWidth = ImGui.CalcTextSize(SeIconChar.Clock.ToIconString()).X; + float crystalWidth = ImGui.CalcTextSize(SeIconChar.Hyadelyn.ToIconString()).X; + float iconsWidth = (showStopClock ? clockWidth : 0f) + + (showPriorityCrystal ? crystalWidth : 0f) + + (showStopClock && showPriorityCrystal ? ImGui.GetStyle().ItemSpacing.X : 0f); + ImGui.SameLine(ImGui.GetWindowContentRegionMax().X - iconsWidth); + } + if (showStopClock) + { // Tooltip color based on status - Vector4 iconColor = ImGuiColors.ParsedPurple; + Vector4 iconColor = QstTheme.Special; if (hasLevelCondition) { @@ -261,9 +287,9 @@ private void DrawQuestNames(QuestController.QuestProgress currentQuest, { short currentLevel = PlayerState.Instance()->CurrentLevel; if (currentLevel > 0 && currentLevel >= _configuration.Stop.TargetLevel) - iconColor = ImGuiColors.ParsedGreen; + iconColor = QstTheme.Success; else if (currentLevel > 0) - iconColor = ImGuiColors.ParsedBlue; + iconColor = QstTheme.Info; } } @@ -285,9 +311,9 @@ private void DrawQuestNames(QuestController.QuestProgress currentQuest, { ImGui.SameLine(); if (currentLevel >= _configuration.Stop.TargetLevel) - ImGui.TextColored(ImGuiColors.ParsedGreen, _LF("(Current: {0} - Reached!)", currentLevel)); + ImGui.TextColored(QstTheme.Success, _LF("(Current: {0} - Reached!)", currentLevel)); else - ImGui.TextColored(ImGuiColors.ParsedBlue, _LF("(Current: {0})", currentLevel)); + ImGui.TextColored(QstTheme.Info, _LF("(Current: {0})", currentLevel)); } } } @@ -334,19 +360,11 @@ private void DrawQuestNames(QuestController.QuestProgress currentQuest, } - List priorityQuests = _questFunctions.NextPriorityQuestsThatCanBeAccepted; - bool anyAvailable = false; - bool anyUnavailable = false; - foreach (var p in priorityQuests) - { - if (p.IsAvailable) anyAvailable = true; - else anyUnavailable = true; - if (anyAvailable && anyUnavailable) break; - } - if (anyAvailable || anyUnavailable) + if (showPriorityCrystal) { - ImGui.SameLine(); - ImGui.TextColored(ImGuiColors.DalamudYellow, SeIconChar.Hyadelyn.ToIconString()); + if (showStopClock) + ImGui.SameLine(); + ImGui.TextColored(QstTheme.Amber, SeIconChar.Hyadelyn.ToIconString()); if (ImGui.IsItemHovered()) { List availablePriorityQuests = priorityQuests @@ -383,19 +401,37 @@ private void DrawQuestNames(QuestController.QuestProgress currentQuest, } } } + + QuestSequence? metaSequence = currentQuest.Quest.FindSequence(currentQuest.Sequence); + using (ImRaii.Disabled()) + { + ImGui.TextUnformatted(_LF("Seq {0} · Step {1}/{2}", + currentQuest.Sequence, currentQuest.Step, metaSequence?.Steps.Count ?? 0)); + } + + if (metaSequence?.FindStep(currentQuest.Step) is { } metaStep) + { + ImGui.SameLine(); + QstWidgets.Chip(metaStep.InteractionType.ToString(), QstTheme.Accent); + if (metaStep.DataId is { } metaDataId) + { + ImGui.SameLine(); + QstWidgets.Chip(metaDataId.ToString(CultureInfo.InvariantCulture), QstTheme.TextMuted); + } + } } QuestController.QuestProgress? nextQuest = _questController.NextQuest; if (nextQuest != null) { - using ImRaii.ColorDisposable _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow); + using ImRaii.ColorDisposable _ = ImRaii.PushColor(ImGuiCol.Text, QstTheme.Amber); ImGui.TextUnformatted( _L("Next Quest: ") + _LF("{0} ({1}) / {2} / {3}", - Shorten(currentQuest.Quest.Info.Name), - currentQuest.Quest.Id, - currentQuest.Sequence, - currentQuest.Step)); + Shorten(nextQuest.Quest.Info.Name), + nextQuest.Quest.Id, + nextQuest.Sequence, + nextQuest.Step)); } } } @@ -417,7 +453,7 @@ private void DrawQuestNames(QuestController.QuestProgress currentQuest, if (ptr != null) color = *ptr; else - color = ImGuiColors.ParsedOrange; + color = QstTheme.Accent; } using ImRaii.ColorDisposable styleColor = ImRaii.PushColor(ImGuiCol.Text, color); @@ -434,9 +470,10 @@ private void DrawQuestNames(QuestController.QuestProgress currentQuest, { ImGui.SetTooltip(questWork.Tooltip); ImGui.SameLine(); - ImGui.PushFont(UiBuilder.IconFont); - ImGui.Text(FontAwesomeIcon.Copy.ToIconString()); - ImGui.PopFont(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGui.Text(FontAwesomeIcon.Copy.ToIconString()); + } } if (currentQuest.Quest.Info.AlliedSociety != EAlliedSociety.None) @@ -461,9 +498,12 @@ private void DrawQuestNames(QuestController.QuestProgress currentQuest, private void DrawQuestButtons(QuestController.QuestProgress currentQuest, QuestStep? currentStep, QuestProgressInfo? questProgressInfo, bool isMinimized) { + if (!isMinimized) + ImGui.Separator(); + using (ImRaii.Disabled(_questController.IsRunning)) { - if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.Play)) + if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.Play, QstTheme.Accent)) { // if we haven't accepted this quest, mark it as next quest so that we can optionally use aetherytes to travel if (questProgressInfo == null) @@ -495,37 +535,31 @@ private void DrawQuestButtons(QuestController.QuestProgress currentQuest, QuestS { ImGui.SameLine(); - if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.FlagCheckered, - _questController.StopAfterCurrentQuest ? ImGuiColors.DalamudOrange : null)) - _questController.StopAfterCurrentQuest = !_questController.StopAfterCurrentQuest; - - if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) - ImGui.SetTooltip(_questController.StopAfterCurrentQuest - ? _L("Cancel scheduled stop after current quest.") - : _L("Stop after the current quest completes.")); - - ImGui.SameLine(); - - if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.Check, - _questController.StopAfterAcceptingNextQuest ? ImGuiColors.DalamudOrange : null)) - _questController.StopAfterAcceptingNextQuest = !_questController.StopAfterAcceptingNextQuest; - - if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) - ImGui.SetTooltip(_questController.StopAfterAcceptingNextQuest - ? _L("Cancel scheduled stop after accepting the next quest.") - : _L("Stop after accepting the next quest.")); + bool anyStopArmed = _questController.StopAfterCurrentQuest + || _questController.StopAfterAcceptingNextQuest + || _questController.StopBeforeTeleport; + using (QstWidgets.SegmentGroup(anyStopArmed)) + { + if (QstWidgets.SegmentToggle(FontAwesomeIcon.FlagCheckered, _questController.StopAfterCurrentQuest, + _L("Cancel scheduled stop after current quest."), + _L("Stop after the current quest completes."))) + _questController.StopAfterCurrentQuest = !_questController.StopAfterCurrentQuest; - if (!isMinimized) ImGui.SameLine(); - if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.MapMarkerAlt, - _questController.StopBeforeTeleport ? ImGuiColors.DalamudOrange : null)) - _questController.StopBeforeTeleport = !_questController.StopBeforeTeleport; + if (QstWidgets.SegmentToggle(FontAwesomeIcon.Check, _questController.StopAfterAcceptingNextQuest, + _L("Cancel scheduled stop after accepting the next quest."), + _L("Stop after accepting the next quest."))) + _questController.StopAfterAcceptingNextQuest = !_questController.StopAfterAcceptingNextQuest; - if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) - ImGui.SetTooltip(_questController.StopBeforeTeleport - ? _L("Cancel scheduled stop before teleport.") - : _L("Stop before the next aetheryte teleport or item use.")); + if (!isMinimized) + ImGui.SameLine(); + + if (QstWidgets.SegmentToggle(FontAwesomeIcon.MapMarkerAlt, _questController.StopBeforeTeleport, + _L("Cancel scheduled stop before teleport."), + _L("Stop before the next aetheryte teleport or item use."))) + _questController.StopBeforeTeleport = !_questController.StopBeforeTeleport; + } } if (isMinimized) @@ -545,7 +579,7 @@ private void DrawQuestButtons(QuestController.QuestProgress currentQuest, QuestS using (ImRaii.Disabled(lastStep)) { - using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedGreen, colored)) + using (ImRaii.PushColor(ImGuiCol.Text, QstTheme.Success, colored)) { if (ImGuiComponentsLocal.IconButtonWithText(FontAwesomeIcon.ArrowCircleRight, _L("Skip"))) { @@ -578,7 +612,7 @@ private void DrawSimulationControls() QuestController.QuestProgress? simulatedQuest = _questController.SimulatedQuest; ImGui.Separator(); - ImGui.TextColored(ImGuiColors.DalamudRed, _L("Quest sim active (experimental)")); + ImGui.TextColored(QstTheme.Danger, _L("Quest sim active (experimental)")); ImGui.Text(_LF("Sequence: {0}", simulatedQuest.Sequence)); using (ImRaii.Disabled(simulatedQuest.Sequence == 0)) @@ -662,6 +696,34 @@ private void DrawSimulationControls() } } + public void DrawTitleBarPill(string windowTitle) + { + if (_combatController.IsRunning) + QstWidgets.TitleBarPill(_L("Combat"), QstTheme.Accent, windowTitle); + else if (_questController.IsRunning + && (_questController.StopAfterCurrentQuest + || _questController.StopAfterAcceptingNextQuest + || _questController.StopBeforeTeleport)) + QstWidgets.TitleBarPill(_L("Stopping"), QstTheme.Amber, windowTitle); + else if (_questController.IsRunning) + QstWidgets.TitleBarPill(_L("Running"), QstTheme.Success, windowTitle); + else + QstWidgets.TitleBarPill(_L("Idle"), QstTheme.TextMuted, windowTitle); + } + + private static float CalculateQuestProgress(QuestController.QuestProgress progress) + { + List sequences = progress.Quest.Root.QuestSequence; + int totalSteps = sequences.Sum(x => x.Steps.Count); + if (totalSteps == 0) + return 0f; + + int currentSequenceSteps = sequences.FirstOrDefault(x => x.Sequence == progress.Sequence)?.Steps.Count ?? 0; + int doneSteps = sequences.Where(x => x.Sequence < progress.Sequence).Sum(x => x.Steps.Count) + + Math.Min(progress.Step, currentSequenceSteps); + return Math.Clamp(doneSteps / (float)totalSteps, 0f, 1f); + } + private static string Shorten(string text) { if (text.Length > 30) diff --git a/Questionable/Windows/QuestComponents/CreationUtilsComponent.cs b/Questionable/Windows/QuestComponents/CreationUtilsComponent.cs index 56583b4a7..6cb3bc013 100644 --- a/Questionable/Windows/QuestComponents/CreationUtilsComponent.cs +++ b/Questionable/Windows/QuestComponents/CreationUtilsComponent.cs @@ -1,9 +1,8 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.Text; using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility.Raii; using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions; using FFXIVClientStructs.FFXIV.Client.Game; @@ -14,6 +13,7 @@ using Questionable.Model.Common; using Questionable.Model.Questing; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows.QuestComponents; @@ -28,6 +28,7 @@ internal sealed class CreationUtilsComponent QuestData questData, QuestSelectionWindow questSelectionWindow, PriorityWindow priorityWindow, + PathEditorWindow pathEditorWindow, RedoUtil redoUtil, IClientState clientState, IObjectTable objectTable, @@ -43,6 +44,12 @@ public void Draw() if (objectTable[0] == null) return; + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGui.TextColored(QstTheme.TextMuted, FontAwesomeIcon.MapMarkerAlt.ToIconString()); + } + + ImGui.SameLine(); string territoryName = TerritoryData.GetNameAndId(clientState.TerritoryType); ImGui.Text(territoryName); @@ -54,7 +61,7 @@ public void Draw() if (configuration.Advanced.AdditionalStatusInformation) { - ImGui.Separator(); + ImGui.Spacing(); QuestReference q = questFunctions.GetCurrentQuest(); ImGui.Text(_LF("QST prio: {0} → {1}", q.CurrentQuest?.ToString() ?? "", q.Sequence)); Quest? simQ = questController.SimulatedQuest?.Quest; @@ -138,7 +145,7 @@ public void Draw() Director* director = UIState.Instance()->DirectorTodo.Director; if (director != null) { - ImGui.Separator(); + ImGui.Spacing(); ImGui.Text(_LF("Director: {0}", director->ContentId)); ImGui.Text(_LF("Seq: {0}", director->Sequence)); ImGui.Text(_LF("Ico: {0}", director->IconId)); @@ -154,7 +161,7 @@ public void Draw() if (configuration.Advanced.ShowActionManager) { - ImGui.Separator(); + ImGui.Spacing(); ActionManager* actionManager = ActionManager.Instance(); ImGui.Text( $"A1: {actionManager->CastActionId} ({actionManager->LastUsedActionSequence} → {actionManager->LastHandledActionSequence})"); @@ -180,11 +187,27 @@ public void Draw() } else { - ImGui.Separator(); + ImGui.Spacing(); DrawInteractionButtons(); ImGui.SameLine(); DrawCopyButton(); } + + ImGui.SameLine(); + DrawPathEditorButton(); + } + + private void DrawPathEditorButton() + { + ElementId? currentQuest = questFunctions.GetCurrentQuest().CurrentQuest; + using (ImRaii.Disabled(currentQuest == null)) + { + if (ImGuiComponentsLocal.IconButton(FontAwesomeIcon.Edit) && currentQuest != null) + pathEditorWindow.Open(currentQuest); + } + + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip(_L("Open the current quest in the Path Editor.")); } private unsafe void DrawTargetDetails(IGameObject target) @@ -193,30 +216,37 @@ private unsafe void DrawTargetDetails(IGameObject target) if (target is ICharacter { NameId: > 0 } character) nameId = $"; n={character.NameId}"; - ImGui.Separator(); - ImGui.Text(_LF("Target: {0}", target.Name)); - ImGui.Text(_LF(" ({0}; {1}{2})", - target.ObjectKind, GameFunctions.GetBaseID(target), nameId)); - - if (objectTable[0] != null) + ImGui.Spacing(); + using (QstWidgets.Card()) { - ImGui.Text(_LF("Distance: {0:F2} ({1}y)", - (target.Position - objectTable[0]!.Position).Length(), - Math.Floor(target.Position.DistanceTo_XZ(objectTable[0]!.Position)) - 1)); + ImGui.Text(_LF("Target: {0}", target.Name)); ImGui.SameLine(); + if (QstWidgets.Chip(QuestStepCapture.GuessInteractionType(target).ToString(), QstTheme.Info)) + ImGui.SetTooltip(_L("Interaction type detected from the target's nameplate icon.")); - float verticalDistance = target.Position.Y - objectTable[0]!.Position.Y; - string verticalDistanceText = _LF("Y: {0:F2}", verticalDistance); - if (Math.Abs(verticalDistance) >= MovementController.DefaultVerticalInteractionDistance) - ImGui.TextColored(ImGuiColors.DalamudOrange, verticalDistanceText); - else - ImGui.Text(verticalDistanceText); + ImGui.Text(_LF(" ({0}; {1}{2})", + target.ObjectKind, GameFunctions.GetBaseID(target), nameId)); - ImGui.SameLine(); - } + if (objectTable[0] != null) + { + ImGui.Text(_LF("Distance: {0:F2} ({1}y)", + (target.Position - objectTable[0]!.Position).Length(), + Math.Floor(target.Position.DistanceTo_XZ(objectTable[0]!.Position)) - 1)); + ImGui.SameLine(); + + float verticalDistance = target.Position.Y - objectTable[0]!.Position.Y; + string verticalDistanceText = _LF("Y: {0:F2}", verticalDistance); + if (Math.Abs(verticalDistance) >= MovementController.DefaultVerticalInteractionDistance) + ImGui.TextColored(QstTheme.Accent, verticalDistanceText); + else + ImGui.Text(verticalDistanceText); - GameObject* gameObject = (GameObject*)target.Address; - ImGui.Text($"QM: {gameObject->NamePlateIconId}"); + ImGui.SameLine(); + } + + GameObject* gameObject = (GameObject*)target.Address; + ImGui.Text($"QM: {gameObject->NamePlateIconId}"); + } } private unsafe void DrawInteractionButtons(IGameObject? target = null) @@ -333,13 +363,7 @@ private unsafe void DrawCopyButton(IGameObject target) } else { - string interactionType = gameObject->NamePlateIconId switch - { - 71201 or 71211 or 71221 or 71231 or 71341 or 71351 => "AcceptQuest", - 71202 or 71212 or 71222 or 71232 or 71342 or 71352 => "AcceptQuest", // repeatable - 71205 or 71215 or 71225 or 71235 or 71345 or 71355 => "CompleteQuest", - var _ => "Interact" - }; + string interactionType = QuestStepCapture.GuessInteractionType(target).ToString(); ImGui.SetClipboardText($$""" "DataId": {{GameFunctions.GetBaseID(target)}}, "Position": { diff --git a/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs b/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs index 162f07245..6b1c9af68 100644 --- a/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs +++ b/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs @@ -1,13 +1,13 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Game.Text; using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility.Raii; using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using Questionable.Model.Common; using Questionable.Model.Questing; using static Questionable.Domain.QuestInfo; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows.QuestComponents; internal sealed class QuestTooltipComponent @@ -33,7 +33,7 @@ public void DrawInner(IQuestInfo questInfo, bool showItemRewards) { string lvlString = $"{SeIconChar.LevelEn.ToIconString()}{questInfo.Level}"; if (PlayerState.Instance()->CurrentLevel < questInfo.Level) - ImGui.TextColored(ImGuiColors.DalamudRed, lvlString); + ImGui.TextColored(QstTheme.Danger, lvlString); else ImGui.Text(lvlString); } @@ -67,7 +67,7 @@ public void DrawInner(IQuestInfo questInfo, bool showItemRewards) if (quest.Root.Disabled) { ImGui.SameLine(); - ImGui.TextColored(ImGuiColors.DalamudRed, _L("Disabled")); + ImGui.TextColored(QstTheme.Danger, _L("Disabled")); } if (quest.Root.Author.Count == 1) @@ -88,7 +88,7 @@ public void DrawInner(IQuestInfo questInfo, bool showItemRewards) else { ImGui.SameLine(); - ImGui.TextColored(ImGuiColors.DalamudRed, _L("NoQuestPath")); + ImGui.TextColored(QstTheme.Danger, _L("NoQuestPath")); if (questInfo is QuestInfo questInfo1) ImGui.Text($"{questInfo1.IssuerLocation.Territory.PlaceName.Value.Name}"); } @@ -169,7 +169,7 @@ private void DrawQuestUnlocks(IQuestInfo questInfo, int counter, bool showItemRe questFunctions.prereqCache[_currentTopLevel.QuestId.Value].Add(qInfo); (Vector4 iconColor, FontAwesomeIcon icon, string _) = uiUtils.GetQuestStyle(q.QuestId); if (!questRegistry.IsKnownQuest(qInfo.QuestId)) - iconColor = ImGuiColors.DalamudGrey; + iconColor = QstTheme.TextMuted; if (!_shownAlready.Contains(qInfo)) { @@ -185,7 +185,7 @@ private void DrawQuestUnlocks(IQuestInfo questInfo, int counter, bool showItemRe else { using ImRaii.DisabledDisposable _ = ImRaii.Disabled(); - uiUtils.ChecklistItem(_LF("Unknown Quest ({0})", q.QuestId), ImGuiColors.DalamudGrey, + uiUtils.ChecklistItem(_LF("Unknown Quest ({0})", q.QuestId), QstTheme.TextMuted, FontAwesomeIcon.Question); } } @@ -215,7 +215,7 @@ private void DrawQuestUnlocks(IQuestInfo questInfo, int counter, bool showItemRe IQuestInfo qInfo = questData.GetQuestInfo(q); (Vector4 iconColor, FontAwesomeIcon icon, string _) = uiUtils.GetQuestStyle(q); if (!questRegistry.IsKnownQuest(qInfo.QuestId)) - iconColor = ImGuiColors.DalamudGrey; + iconColor = QstTheme.TextMuted; uiUtils.ChecklistItem(FormatQuestUnlockName(qInfo), iconColor, icon); } diff --git a/Questionable/Windows/QuestComponents/QuickAccessButtonsComponent.cs b/Questionable/Windows/QuestComponents/QuickAccessButtonsComponent.cs index 7a9cd2f10..fba925ab3 100644 --- a/Questionable/Windows/QuestComponents/QuickAccessButtonsComponent.cs +++ b/Questionable/Windows/QuestComponents/QuickAccessButtonsComponent.cs @@ -1,13 +1,13 @@ -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Nodes; using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Questionable.Controller.Steps.Shared; using Questionable.Model.Questing; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows.QuestComponents; internal sealed class QuickAccessButtonsComponent @@ -31,11 +31,11 @@ public void Draw() DrawPriorityQuestsButton(); ImGui.SameLine(); DrawJournalProgressButton(); - + ImGui.SameLine(); DrawReloadDataButton(); ImGui.SameLine(); DrawRebuildNavmeshButton(); - + ImGui.SameLine(); DrawTroubleshootingButton(questController.CurrentQuest, questController.IsRunning); if (pluginInterface.IsDev) @@ -47,54 +47,42 @@ public void Draw() private void DrawPriorityQuestsButton() { - using var _ = ImRaii.Disabled(objectTable[0] == null); - if (ImGuiComponentsLocal.IconButtonWithText(FontAwesomeIcon.ExclamationCircle, _L("Priority Quests"))) + if (QstWidgets.RailButton(FontAwesomeIcon.ExclamationCircle, + _L("Configure priority quests which will be done as soon as possible."), + enabled: objectTable[0] != null)) priorityWindow.ToggleOrUncollapse(); - - if (ImGui.IsItemHovered()) - ImGui.SetTooltip(_L("Configure priority quests which will be done as soon as possible.")); } private void DrawRebuildNavmeshButton() { bool isNavmeshAvailable = commandManager.Commands.ContainsKey("/vnav"); - using (ImRaii.Disabled(!isNavmeshAvailable || !ImGui.IsKeyDown(ImGuiKey.ModCtrl))) - { - if (ImGuiComponentsLocal.IconButtonWithText(FontAwesomeIcon.GlobeEurope, _L("Rebuild Navmesh"))) - commandManager.ProcessCommand("/vnav rebuild"); - } - - if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) - { - if (!isNavmeshAvailable) - ImGui.SetTooltip(_L("vnavmesh is not available. Please install it first.")); - else - ImGui.SetTooltip(_L("Hold CTRL to enable this button. Rebuilding the navmesh will take some time.")); - } + string tooltip = isNavmeshAvailable + ? _L("Hold CTRL to enable this button. Rebuilding the navmesh will take some time.") + : _L("vnavmesh is not available. Please install it first."); + if (QstWidgets.RailButton(FontAwesomeIcon.GlobeEurope, tooltip, + enabled: isNavmeshAvailable && ImGui.IsKeyDown(ImGuiKey.ModCtrl))) + commandManager.ProcessCommand("/vnav rebuild"); } private void DrawReloadDataButton() { - if (ImGuiComponentsLocal.IconButtonWithText(FontAwesomeIcon.RedoAlt, _L("Reload Data"))) + if (QstWidgets.RailButton(FontAwesomeIcon.RedoAlt, _L("Reload Data"))) Reload?.Invoke(this, EventArgs.Empty); } private void DrawJournalProgressButton() { - if (ImGuiComponentsLocal.IconButtonWithText(FontAwesomeIcon.BookBookmark, _L("Journal Progress"))) + if (QstWidgets.RailButton(FontAwesomeIcon.BookBookmark, _L("Journal Progress"))) journalProgressWindow.ToggleOrUncollapse(); - - if (ImGui.IsItemHovered()) - ImGui.SetTooltip(_L("Journal Progress")); } private void DrawTroubleshootingButton(QuestController.QuestProgress? questProgress, bool isRunning) { - using var _ = ImRaii.Disabled(objectTable[0] == null); - bool leftClicked = ImGuiComponentsLocal.IconButtonWithText(FontAwesomeIcon.Handshake, _L("Stuck?"), isRunning ? ImGuiColors.DalamudOrange : null); + bool leftClicked = QstWidgets.RailButton(FontAwesomeIcon.Handshake, + _L("Left click: Copy troubleshooting information to clipboard\nRight click: Copy list of completed quests to clipboard"), + tint: isRunning ? QstTheme.Accent : null, + enabled: objectTable[0] != null); bool rightClicked = ImGui.IsItemClicked(ImGuiMouseButton.Right); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip(_L("Left click: Copy troubleshooting information to clipboard\nRight click: Copy list of completed quests to clipboard")); if (leftClicked || rightClicked) { string output = ""; @@ -168,65 +156,18 @@ private void DrawValidationIssuesButton() { int errorCount = questRegistry.ValidationErrorCount; int infoCount = questRegistry.ValidationIssueCount - questRegistry.ValidationErrorCount; - - int partsToRender = errorCount == 0 ? 1 : 2; - using ImRaii.IdDisposable id = ImRaii.PushId("validationissues"); - - FontAwesomeIcon icon1 = FontAwesomeIcon.ExclamationTriangle; - FontAwesomeIcon icon2 = FontAwesomeIcon.InfoCircle; - Vector2 iconSize1, iconSize2; - using (IDisposable _ = pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) - { - iconSize1 = errorCount > 0 ? ImGui.CalcTextSize(icon1.ToIconString()) : Vector2.Zero; - iconSize2 = infoCount >= 0 ? ImGui.CalcTextSize(icon2.ToIconString()) : Vector2.Zero; - } - - string text1 = errorCount > 0 ? errorCount.ToString(CultureInfo.InvariantCulture) : string.Empty; - string text2 = infoCount > 0 ? infoCount.ToString(CultureInfo.InvariantCulture) : "..."; - Vector2 textSize1 = errorCount > 0 ? ImGui.CalcTextSize(text1) : Vector2.Zero; - Vector2 textSize2 = infoCount >= 0 ? ImGui.CalcTextSize(text2) : Vector2.Zero; - ImDrawListPtr dl = ImGui.GetWindowDrawList(); - Vector2 cursor = ImGui.GetCursorScreenPos(); - - float iconPadding = 3 * ImGuiHelpers.GlobalScale; - - // Draw an ImGui button with the icon and text - float buttonWidth = iconSize1.X + iconSize2.X + textSize1.X + textSize2.X + - (ImGui.GetStyle().FramePadding.X * 2) + iconPadding * 2 * partsToRender; - float buttonHeight = ImGui.GetFrameHeight(); - bool button = ImGui.Button(string.Empty, new(buttonWidth, buttonHeight)); - - // Draw the icon on the window drawlist - Vector2 position = new(cursor.X + ImGui.GetStyle().FramePadding.X, - cursor.Y + ImGui.GetStyle().FramePadding.Y); - if (errorCount > 0) - { - using (IDisposable _ = pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) - { - dl.AddText(position, ImGui.GetColorU32(ImGuiColors.DalamudRed), icon1.ToIconString()); - } - - position = position with { X = position.X + iconSize1.X + iconPadding }; - - // Draw the text on the window drawlist - dl.AddText(position, ImGui.GetColorU32(ImGuiCol.Text), text1); - position = position with { X = position.X + textSize1.X + 2 * iconPadding }; - } - - if (infoCount >= 0) - { - using (IDisposable _ = pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) - { - dl.AddText(position, ImGui.GetColorU32(ImGuiColors.ParsedBlue), icon2.ToIconString()); - } - - position = position with { X = position.X + iconSize2.X + iconPadding }; - - // Draw the text on the window drawlist - dl.AddText(position, ImGui.GetColorU32(ImGuiCol.Text), text2); - } - - if (button) + bool hasErrors = errorCount > 0; + + string badge = hasErrors + ? errorCount.ToString(CultureInfo.InvariantCulture) + : infoCount > 0 + ? infoCount.ToString(CultureInfo.InvariantCulture) + : "..."; + if (QstWidgets.RailButton(hasErrors ? FontAwesomeIcon.ExclamationTriangle : FontAwesomeIcon.InfoCircle, + _LF("Quest validation: {0} errors, {1} infos", errorCount, infoCount), + tint: hasErrors ? QstTheme.Danger : QstTheme.Info, + countBadge: badge, + badgeColor: hasErrors ? QstTheme.Danger : QstTheme.Info)) questValidationWindow.ToggleOrUncollapse(); } } diff --git a/Questionable/Windows/QuestComponents/RemainingTasksComponent.cs b/Questionable/Windows/QuestComponents/RemainingTasksComponent.cs index 0ce3a5a5c..471fa0f79 100644 --- a/Questionable/Windows/QuestComponents/RemainingTasksComponent.cs +++ b/Questionable/Windows/QuestComponents/RemainingTasksComponent.cs @@ -1,5 +1,7 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows.QuestComponents; internal sealed class RemainingTasksComponent( @@ -11,28 +13,42 @@ public void Draw() { if (configuration.General.HideRemainingTasks) return; + IList gatheringTasks = gatheringController.GetRemainingTaskNames(); - if (gatheringTasks.Count > 0) - { - ImGui.Separator(); - using (ImRaii.Disabled()) - { - foreach (string task in gatheringTasks) - ImGui.TextUnformatted($"G: {task}"); - } - } - else + bool isGathering = gatheringTasks.Count > 0; + IList tasks = isGathering ? gatheringTasks : questController.GetRemainingTaskNames(); + if (tasks.Count == 0) + return; + + if (!QstWidgets.SectionHeader(_L("Remaining Tasks"), "RemainingTasks", count: tasks.Count)) + return; + + using (ImRaii.PushFont(UiBuilder.MonoFont)) { - IList remainingTasks = questController.GetRemainingTaskNames(); - if (remainingTasks.Count > 0) + for (int i = 0; i < tasks.Count; i++) { - ImGui.Separator(); - using (ImRaii.Disabled()) + string task = isGathering ? $"G: {tasks[i]}" : tasks[i]; + if (i == 0 && questController.IsRunning) + ImGui.TextColored(QstTheme.Accent, Truncate(task)); + else { - foreach (string task in remainingTasks) - ImGui.TextUnformatted(task); + using ImRaii.DisabledDisposable _ = ImRaii.Disabled(); + ImGui.TextUnformatted(Truncate(task)); } + + if (task.Length > MaxTaskLength && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip(task); } } } + + private const int MaxTaskLength = 44; + + private static string Truncate(string text) + { + if (text.Length <= MaxTaskLength) + return text; + + return text[..(MaxTaskLength - 3)].TrimEnd() + "..."; + } } diff --git a/Questionable/Windows/QuestComponents/ReportWarningComponent.cs b/Questionable/Windows/QuestComponents/ReportWarningComponent.cs index e1357b874..9045b61a0 100644 --- a/Questionable/Windows/QuestComponents/ReportWarningComponent.cs +++ b/Questionable/Windows/QuestComponents/ReportWarningComponent.cs @@ -1,6 +1,6 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using Dalamud.Interface.Colors; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows.QuestComponents; internal sealed class ReportWarningComponent(Configuration configuration) @@ -11,7 +11,7 @@ internal sealed class ReportWarningComponent(Configuration configuration) private void DrawReportWarning() { - ImGui.TextColored(ImGuiColors.DPSRed, "Future message"); + ImGui.TextColored(QstTheme.Danger, "Future message"); ImGui.TextWrapped("As of version xxxx, QST includes a feature where you can click the " + "! button next to the quest progress buttons to report an issue with the current quest. " + "This message is to notify you that if you choose to make use of this new feature and submit a " + @@ -24,14 +24,14 @@ private void DrawReportWarning() ImGui.TextWrapped("This feature will never send any information to the bug report service unless you click " + "the ! button highlighted in red below. If you would like to opt out of seeing this button, click the " + "orange \"Opt Out\" button below. Otherwise, click the green \"Dismiss\" button to hide this warning."); - if (ImGuiComponentsLocal.IconButtonWithText(FontAwesomeIcon.ExclamationTriangle, "Opt Out", ImGuiColors.DalamudOrange)) + if (ImGuiComponentsLocal.IconButtonWithText(FontAwesomeIcon.ExclamationTriangle, "Opt Out", QstTheme.Accent)) { _configuration.General.DismissedReportWarning = true; _configuration.General.ReportsDisabled = true; } ImGui.SameLine(); - if (ImGuiComponentsLocal.IconButtonWithText(FontAwesomeIcon.ExclamationTriangle, "Dismiss", ImGuiColors.ParsedGreen)) + if (ImGuiComponentsLocal.IconButtonWithText(FontAwesomeIcon.ExclamationTriangle, "Dismiss", QstTheme.Success)) _configuration.General.DismissedReportWarning = true; } } diff --git a/Questionable/Windows/QuestSelectionWindow.cs b/Questionable/Windows/QuestSelectionWindow.cs index d11909702..6c777c70b 100644 --- a/Questionable/Windows/QuestSelectionWindow.cs +++ b/Questionable/Windows/QuestSelectionWindow.cs @@ -1,12 +1,12 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility.Raii; using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.UI; using Questionable.Model.Questing; using Questionable.Windows.Common; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows; internal sealed class QuestSelectionWindow : LWindow @@ -169,7 +169,7 @@ public override void DrawContent() if (isKnownQuest) ImGui.TextColored(color, icon.ToIconString()); else - ImGui.TextColored(ImGuiColors.DalamudGrey, icon.ToIconString()); + ImGui.TextColored(QstTheme.TextMuted, icon.ToIconString()); } if (ImGui.IsItemHovered()) @@ -183,7 +183,7 @@ public override void DrawContent() if (knownQuest != null && knownQuest.Root.Disabled) { using IDisposable _ = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push(); - ImGui.TextColored(ImGuiColors.DalamudOrange, FontAwesomeIcon.Ban.ToIconString()); + ImGui.TextColored(QstTheme.Accent, FontAwesomeIcon.Ban.ToIconString()); ImGui.SameLine(); } diff --git a/Questionable/Windows/QuestValidationWindow.cs b/Questionable/Windows/QuestValidationWindow.cs index a2c4b3736..a757492fe 100644 --- a/Questionable/Windows/QuestValidationWindow.cs +++ b/Questionable/Windows/QuestValidationWindow.cs @@ -1,9 +1,9 @@ -using System.Text.Json; +using System.Text.Json; using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility.Raii; using Questionable.Windows.Common; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows; internal sealed class QuestValidationWindow : LWindow @@ -14,6 +14,7 @@ internal sealed class QuestValidationWindow : LWindow private readonly QuestValidator _questValidator; private readonly QuestTooltipComponent _questTooltipComponent; private readonly RedoUtil _redoUtil; + private readonly PathEditorWindow _pathEditorWindow; private string _filter = ""; public QuestValidationWindow( @@ -22,6 +23,7 @@ public QuestValidationWindow( QuestController questController, QuestTooltipComponent questTooltipComponent, RedoUtil redoUtil, + PathEditorWindow pathEditorWindow, IDalamudPluginInterface pluginInterface) : base(_L("Quest Validation") + "###QuestionableValidator") { @@ -31,6 +33,7 @@ public QuestValidationWindow( _pluginInterface = pluginInterface; _questTooltipComponent = questTooltipComponent; _redoUtil = redoUtil; + _pathEditorWindow = pathEditorWindow; Size = new Vector2(600, 200); SizeCondition = ImGuiCond.Once; @@ -95,9 +98,12 @@ public override void DrawContent() ImGui.SameLine(); bool edit = ImGuiComponentsLocal.IconButton($"###ValidationWindowEdit{quest.QuestId.Value}", FontAwesomeIcon.Edit); + bool editExternal = ImGui.IsItemClicked(ImGuiMouseButton.Right); if (ImGui.IsItemHovered()) - ImGui.SetTooltip(QuestRegistry.OpenEditorDescription); + ImGui.SetTooltip(_L("Left click: Open in Path Editor\nRight click: Open in your default .json text editor")); if (edit) + _pathEditorWindow.Open(quest.QuestId); + else if (editExternal) QuestRegistry.OpenEditor(quest); RedoIndex redoIndex = _redoUtil.GetChapter(quest.QuestId.Value); @@ -140,12 +146,12 @@ public override void DrawContent() { if (validationIssue.Severity == EIssueSeverity.Error) { - using ImRaii.ColorDisposable color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + using ImRaii.ColorDisposable color = ImRaii.PushColor(ImGuiCol.Text, QstTheme.Danger); ImGui.TextUnformatted(FontAwesomeIcon.ExclamationTriangle.ToIconString()); } else { - using ImRaii.ColorDisposable color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedBlue); + using ImRaii.ColorDisposable color = ImRaii.PushColor(ImGuiCol.Text, QstTheme.Info); ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString()); } } diff --git a/Questionable/Windows/QuestWindow.cs b/Questionable/Windows/QuestWindow.cs index dc31d7ce8..45228fb11 100644 --- a/Questionable/Windows/QuestWindow.cs +++ b/Questionable/Windows/QuestWindow.cs @@ -1,8 +1,9 @@ -using System.Diagnostics; +using System.Diagnostics; using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility.Raii; using Questionable.Windows.Common; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows; internal sealed class QuestWindow : LWindow, IPersistableWindowConfig @@ -66,7 +67,7 @@ public QuestWindow(IDalamudPluginInterface pluginInterface, SizeConstraints = new WindowSizeConstraints { - MinimumSize = new(240, 30), + MinimumSize = new(300, 30), MaximumSize = default }; RespectCloseHotkey = false; @@ -94,9 +95,8 @@ public QuestWindow(IDalamudPluginInterface pluginInterface, Priority = int.MinValue, ShowTooltip = () => { - ImGui.BeginTooltip(); + using ImRaii.TooltipDisposable _ = ImRaii.Tooltip(); ImGui.Text(_L("Open Configuration")); - ImGui.EndTooltip(); } }); @@ -109,9 +109,8 @@ public QuestWindow(IDalamudPluginInterface pluginInterface, Priority = int.MinValue, ShowTooltip = () => { - ImGui.BeginTooltip(); + using ImRaii.TooltipDisposable _ = ImRaii.Tooltip(); ImGui.Text(_L("Sponsor QST development")); - ImGui.EndTooltip(); } }); @@ -170,42 +169,40 @@ public override void DrawContent() notice = _L("Questionable is not compatible with BossMod Reborn!"); if (notice.Length != 0) { - ImGui.TextColored(ImGuiColors.DPSRed, _L("Notice")); + ImGui.TextColored(QstTheme.Danger, _L("Notice")); ImGui.TextWrapped(_L(notice)); ImGui.Separator(); } + _activeQuestComponent.DrawTitleBarPill(WindowName); _activeQuestComponent.Draw(IsMinimized); if (!IsMinimized) { - ImGui.Separator(); - if (false) { // TODO add tests } - if (_aRealmRebornComponent.ShouldDraw) - { + if (_aRealmRebornComponent.ShouldDraw + && QstWidgets.SectionHeader(_L("A Realm Reborn"), "ARealmReborn")) _aRealmRebornComponent.Draw(); - ImGui.Separator(); - } - if (_eventInfoComponent.ShouldDraw) - { + if (_eventInfoComponent.ShouldDraw + && QstWidgets.SectionHeader(_L("Events"), "Events")) _eventInfoComponent.Draw(); - ImGui.Separator(); - } - _quickAccessButtonsComponent.Draw(); - ImGui.Separator(); - _creationUtilsComponent.Draw(); + if (QstWidgets.SectionHeader(_L("Quick Access"), "QuickAccess")) + _quickAccessButtonsComponent.Draw(); + + if (QstWidgets.SectionHeader(_L("Path Tools"), "PathTools", defaultOpen: false)) + _creationUtilsComponent.Draw(); + _remainingTasksComponent.Draw(); } } catch (Exception e) { - ImGui.TextColored(ImGuiColors.DalamudRed, e.ToString()); + ImGui.TextColored(QstTheme.Danger, e.ToString()); } } diff --git a/Questionable/Windows/UiUtils.cs b/Questionable/Windows/UiUtils.cs index ba1c07e67..255585229 100644 --- a/Questionable/Windows/UiUtils.cs +++ b/Questionable/Windows/UiUtils.cs @@ -1,8 +1,8 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using Dalamud.Interface.Colors; using FFXIVClientStructs.FFXIV.Client.Game.UI; using Questionable.Model.Questing; +using Questionable.Windows.Common.Ui; namespace Questionable.Windows; internal sealed class UiUtils(QuestFunctions questFunctions, IDalamudPluginInterface pluginInterface) @@ -18,37 +18,37 @@ internal sealed class UiUtils(QuestFunctions questFunctions, IDalamudPluginInter lockedReason = _L("Prev quest"); if (questFunctions.IsQuestAccepted(elementId)) - return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight, _L("Active")); + return (QstTheme.Amber, FontAwesomeIcon.PersonWalkingArrowRight, _L("Active")); if (elementId is QuestId questId && questFunctions.IsDailyAlliedSocietyQuestAndAvailableToday(questId)) { if (!questFunctions.IsReadyToAcceptQuest(questId)) - return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check, _L("Complete")); + return (QstTheme.Success, FontAwesomeIcon.Check, _L("Complete")); if (questFunctions.IsQuestComplete(questId)) - return (ImGuiColors.ParsedBlue, FontAwesomeIcon.Running, _L("Available")); + return (QstTheme.Info, FontAwesomeIcon.Running, _L("Available")); - return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running, _L("Available")); + return (QstTheme.Amber, FontAwesomeIcon.Running, _L("Available")); } if (questFunctions.IsQuestAcceptedOrComplete(elementId)) - return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check, _L("Complete")); + return (QstTheme.Success, FontAwesomeIcon.Check, _L("Complete")); if (questFunctions.IsQuestUnobtainable(elementId)) - return (ImGuiColors.DalamudGrey, FontAwesomeIcon.Minus, _L("Unobtainable")); + return (QstTheme.TextMuted, FontAwesomeIcon.Minus, _L("Unobtainable")); if (!string.IsNullOrEmpty(lockedReason)) - return (ImGuiColors.DalamudRed, FontAwesomeIcon.Times, $"{_L("Locked")}: {lockedReason}"); + return (QstTheme.Danger, FontAwesomeIcon.Times, $"{_L("Locked")}: {lockedReason}"); if (prereqValue == null) - return (ImGuiColors.TankBlue, FontAwesomeIcon.QuestionCircle, _L("Available(?)")); + return (QstTheme.Info, FontAwesomeIcon.QuestionCircle, _L("Available(?)")); - return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running, _L("Available")); + return (QstTheme.Amber, FontAwesomeIcon.Running, _L("Available")); } public static (Vector4 color, FontAwesomeIcon icon) GetInstanceStyle(ushort instanceId) { if (UIState.IsInstanceContentCompleted(instanceId)) - return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check); + return (QstTheme.Success, FontAwesomeIcon.Check); if (UIState.IsInstanceContentUnlocked(instanceId)) - return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running); + return (QstTheme.Amber, FontAwesomeIcon.Running); - return (ImGuiColors.DalamudRed, FontAwesomeIcon.Times); + return (QstTheme.Danger, FontAwesomeIcon.Times); } public bool ChecklistItem(string text, Vector4 color, FontAwesomeIcon icon, float extraPadding = 0) @@ -74,7 +74,7 @@ public bool ChecklistItem(string text, Vector4 color, FontAwesomeIcon icon, floa public bool ChecklistItem(string text, bool complete, Vector4? colorOverride = null) { return ChecklistItem(text, - colorOverride ?? (complete ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed), + colorOverride ?? (complete ? QstTheme.Success : QstTheme.Danger), complete ? FontAwesomeIcon.Check : FontAwesomeIcon.Times); } } diff --git a/Questionable/Windows/Utils/QuestStepCapture.cs b/Questionable/Windows/Utils/QuestStepCapture.cs new file mode 100644 index 000000000..732d6e002 --- /dev/null +++ b/Questionable/Windows/Utils/QuestStepCapture.cs @@ -0,0 +1,37 @@ +using Dalamud.Game.ClientState.Objects.Types; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Questionable.Model.Questing; + +namespace Questionable.Windows.Utils; + +internal static class QuestStepCapture +{ + public static unsafe EInteractionType GuessInteractionType(IGameObject target) + { + var gameObject = (GameObject*)target.Address; + return gameObject->NamePlateIconId switch + { + 71201 or 71211 or 71221 or 71231 or 71341 or 71351 => EInteractionType.AcceptQuest, + 71202 or 71212 or 71222 or 71232 or 71342 or 71352 => EInteractionType.AcceptQuest, + 71205 or 71215 or 71225 or 71235 or 71345 or 71355 => EInteractionType.CompleteQuest, + _ => EInteractionType.Interact + }; + } + + public static QuestStep CreateStepFromTarget(IGameObject target, uint territoryType) + { + return new QuestStep(GuessInteractionType(target), GameFunctions.GetBaseID(target), target.Position, + territoryType) + { + Fly = GameFunctions.IsFlyingUnlocked(territoryType) ? true : null + }; + } + + public static QuestStep CreatePositionStep(Vector3 position, uint territoryType) + { + return new QuestStep(EInteractionType.WalkTo, null, position, territoryType) + { + Fly = GameFunctions.IsFlyingUnlocked(territoryType) ? true : null + }; + } +}