mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
Merge remote-tracking branch 'origin/master' into v9-rollup
This commit is contained in:
commit
871d0d21a2
38 changed files with 667 additions and 477 deletions
|
|
@ -5,6 +5,7 @@ using System.Linq;
|
|||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Game.Command;
|
||||
|
|
@ -14,6 +15,7 @@ using Dalamud.Interface.Utility;
|
|||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Plugin.Internal;
|
||||
using Dalamud.Utility;
|
||||
using ImGuiNET;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
|
@ -28,26 +30,26 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
private readonly List<LogEntry> logText = new();
|
||||
private readonly object renderLock = new();
|
||||
|
||||
private readonly string[] logLevelStrings = new[] { "Verbose", "Debug", "Information", "Warning", "Error", "Fatal" };
|
||||
|
||||
private List<LogEntry> filteredLogText = new();
|
||||
private bool autoScroll;
|
||||
private bool openAtStartup;
|
||||
private readonly List<string> history = new();
|
||||
private readonly List<PluginFilterEntry> pluginFilters = new();
|
||||
|
||||
private bool? lastCmdSuccess;
|
||||
|
||||
private string commandText = string.Empty;
|
||||
|
||||
private string textFilter = string.Empty;
|
||||
private int levelFilter;
|
||||
private List<string> sourceFilters = new();
|
||||
private bool filterShowUncaughtExceptions = false;
|
||||
private bool isFiltered = false;
|
||||
private string selectedSource = "DalamudInternal";
|
||||
|
||||
private bool filterShowUncaughtExceptions;
|
||||
private bool showFilterToolbar;
|
||||
private bool clearLog;
|
||||
private bool copyLog;
|
||||
private bool copyMode;
|
||||
private bool killGameArmed;
|
||||
private bool autoScroll;
|
||||
private bool autoOpen;
|
||||
|
||||
private int historyPos;
|
||||
private List<string> history = new();
|
||||
|
||||
private bool killGameArmed = false;
|
||||
private int copyStart = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConsoleWindow"/> class.
|
||||
|
|
@ -58,16 +60,22 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
var configuration = Service<DalamudConfiguration>.Get();
|
||||
|
||||
this.autoScroll = configuration.LogAutoScroll;
|
||||
this.openAtStartup = configuration.LogOpenAtStartup;
|
||||
this.autoOpen = configuration.LogOpenAtStartup;
|
||||
SerilogEventSink.Instance.LogLine += this.OnLogLine;
|
||||
|
||||
this.Size = new Vector2(500, 400);
|
||||
this.SizeCondition = ImGuiCond.FirstUseEver;
|
||||
|
||||
this.SizeConstraints = new WindowSizeConstraints
|
||||
{
|
||||
MinimumSize = new Vector2(600.0f, 200.0f),
|
||||
MaximumSize = new Vector2(9999.0f, 9999.0f),
|
||||
};
|
||||
|
||||
this.RespectCloseHotkey = false;
|
||||
}
|
||||
|
||||
private List<LogEntry> LogEntries => this.isFiltered ? this.filteredLogText : this.logText;
|
||||
private List<LogEntry> FilteredLogEntries { get; set; } = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnOpen()
|
||||
|
|
@ -92,10 +100,20 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
lock (this.renderLock)
|
||||
{
|
||||
this.logText.Clear();
|
||||
this.filteredLogText.Clear();
|
||||
this.FilteredLogEntries.Clear();
|
||||
this.clearLog = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the entire log contents to clipboard.
|
||||
/// </summary>
|
||||
public void CopyLog()
|
||||
{
|
||||
ImGui.LogToClipboard();
|
||||
this.copyLog = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a single log line to the display.
|
||||
/// </summary>
|
||||
|
|
@ -123,157 +141,15 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
/// <inheritdoc/>
|
||||
public override void Draw()
|
||||
{
|
||||
// Options menu
|
||||
if (ImGui.BeginPopup("Options"))
|
||||
{
|
||||
var configuration = Service<DalamudConfiguration>.Get();
|
||||
this.DrawOptionsToolbar();
|
||||
|
||||
if (ImGui.Checkbox("Auto-scroll", ref this.autoScroll))
|
||||
{
|
||||
configuration.LogAutoScroll = this.autoScroll;
|
||||
configuration.QueueSave();
|
||||
}
|
||||
|
||||
if (ImGui.Checkbox("Open at startup", ref this.openAtStartup))
|
||||
{
|
||||
configuration.LogOpenAtStartup = this.openAtStartup;
|
||||
configuration.QueueSave();
|
||||
}
|
||||
|
||||
var prevLevel = (int)EntryPoint.LogLevelSwitch.MinimumLevel;
|
||||
if (ImGui.Combo("Log Level", ref prevLevel, Enum.GetValues(typeof(LogEventLevel)).Cast<LogEventLevel>().Select(x => x.ToString()).ToArray(), 6))
|
||||
{
|
||||
EntryPoint.LogLevelSwitch.MinimumLevel = (LogEventLevel)prevLevel;
|
||||
configuration.LogLevel = (LogEventLevel)prevLevel;
|
||||
configuration.QueueSave();
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
// Filter menu
|
||||
if (ImGui.BeginPopup("Filters"))
|
||||
{
|
||||
if (ImGui.Checkbox("Enabled", ref this.isFiltered))
|
||||
{
|
||||
this.Refilter();
|
||||
}
|
||||
|
||||
if (ImGui.InputTextWithHint("##filterText", "Text Filter", ref this.textFilter, 255, ImGuiInputTextFlags.EnterReturnsTrue))
|
||||
{
|
||||
this.Refilter();
|
||||
}
|
||||
|
||||
ImGui.TextColored(ImGuiColors.DalamudGrey, "Enter to confirm.");
|
||||
|
||||
if (ImGui.BeginCombo("Levels", this.levelFilter == 0 ? "All Levels..." : "Selected Levels..."))
|
||||
{
|
||||
for (var i = 0; i < this.logLevelStrings.Length; i++)
|
||||
{
|
||||
if (ImGui.Selectable(this.logLevelStrings[i], ((this.levelFilter >> i) & 1) == 1))
|
||||
{
|
||||
this.levelFilter ^= 1 << i;
|
||||
this.Refilter();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndCombo();
|
||||
}
|
||||
|
||||
// Filter by specific plugin(s)
|
||||
var pluginInternalNames = Service<PluginManager>.Get().InstalledPlugins
|
||||
.Select(p => p.Manifest.InternalName)
|
||||
.OrderBy(s => s).ToList();
|
||||
var sourcePreviewVal = this.sourceFilters.Count switch
|
||||
{
|
||||
0 => "All plugins...",
|
||||
1 => "1 plugin...",
|
||||
_ => $"{this.sourceFilters.Count} plugins...",
|
||||
};
|
||||
var sourceSelectables = pluginInternalNames.Union(this.sourceFilters).ToList();
|
||||
if (ImGui.BeginCombo("Plugins", sourcePreviewVal))
|
||||
{
|
||||
foreach (var selectable in sourceSelectables)
|
||||
{
|
||||
if (ImGui.Selectable(selectable, this.sourceFilters.Contains(selectable)))
|
||||
{
|
||||
if (!this.sourceFilters.Contains(selectable))
|
||||
{
|
||||
this.sourceFilters.Add(selectable);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.sourceFilters.Remove(selectable);
|
||||
}
|
||||
|
||||
this.Refilter();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndCombo();
|
||||
}
|
||||
|
||||
if (ImGui.Checkbox("Always Show Uncaught Exceptions", ref this.filterShowUncaughtExceptions))
|
||||
{
|
||||
this.Refilter();
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGuiComponents.IconButton(FontAwesomeIcon.Cog))
|
||||
ImGui.OpenPopup("Options");
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip("Options");
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGuiComponents.IconButton(FontAwesomeIcon.Search))
|
||||
ImGui.OpenPopup("Filters");
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip("Filters");
|
||||
|
||||
ImGui.SameLine();
|
||||
var clear = ImGuiComponents.IconButton(FontAwesomeIcon.Trash);
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip("Clear Log");
|
||||
|
||||
ImGui.SameLine();
|
||||
var copy = ImGuiComponents.IconButton(FontAwesomeIcon.Copy);
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip("Copy Log");
|
||||
|
||||
ImGui.SameLine();
|
||||
if (this.killGameArmed)
|
||||
{
|
||||
if (ImGuiComponents.IconButton(FontAwesomeIcon.Flushed))
|
||||
Process.GetCurrentProcess().Kill();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ImGuiComponents.IconButton(FontAwesomeIcon.Skull))
|
||||
this.killGameArmed = true;
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip("Kill game");
|
||||
this.DrawFilterToolbar();
|
||||
|
||||
ImGui.BeginChild("scrolling", new Vector2(0, ImGui.GetFrameHeightWithSpacing() - 55), false, ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar);
|
||||
|
||||
if (clear)
|
||||
{
|
||||
this.Clear();
|
||||
}
|
||||
if (this.clearLog) this.Clear();
|
||||
|
||||
if (copy)
|
||||
{
|
||||
ImGui.LogToClipboard();
|
||||
}
|
||||
if (this.copyLog) this.CopyLog();
|
||||
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
|
||||
|
||||
|
|
@ -289,27 +165,40 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
var childDrawList = ImGui.GetWindowDrawList();
|
||||
var childSize = ImGui.GetWindowSize();
|
||||
|
||||
var cursorDiv = ImGuiHelpers.GlobalScale * 92;
|
||||
var cursorDiv = ImGuiHelpers.GlobalScale * 93;
|
||||
var cursorLogLevel = ImGuiHelpers.GlobalScale * 100;
|
||||
var cursorLogLine = ImGuiHelpers.GlobalScale * 135;
|
||||
|
||||
lock (this.renderLock)
|
||||
{
|
||||
clipper.Begin(this.LogEntries.Count);
|
||||
clipper.Begin(this.FilteredLogEntries.Count);
|
||||
while (clipper.Step())
|
||||
{
|
||||
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
|
||||
{
|
||||
var line = this.LogEntries[i];
|
||||
var line = this.FilteredLogEntries[i];
|
||||
|
||||
if (!line.IsMultiline && !copy)
|
||||
if (!line.IsMultiline && !this.copyLog)
|
||||
ImGui.Separator();
|
||||
|
||||
if (line.SelectedForCopy)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.ParsedGrey);
|
||||
ImGui.PushStyleColor(ImGuiCol.HeaderActive, ImGuiColors.ParsedGrey);
|
||||
ImGui.PushStyleColor(ImGuiCol.HeaderHovered, ImGuiColors.ParsedGrey);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Header, this.GetColorForLogEventLevel(line.Level));
|
||||
ImGui.PushStyleColor(ImGuiCol.HeaderActive, this.GetColorForLogEventLevel(line.Level));
|
||||
ImGui.PushStyleColor(ImGuiCol.HeaderHovered, this.GetColorForLogEventLevel(line.Level));
|
||||
}
|
||||
|
||||
ImGui.PushStyleColor(ImGuiCol.Header, this.GetColorForLogEventLevel(line.Level));
|
||||
ImGui.PushStyleColor(ImGuiCol.HeaderActive, this.GetColorForLogEventLevel(line.Level));
|
||||
ImGui.PushStyleColor(ImGuiCol.HeaderHovered, this.GetColorForLogEventLevel(line.Level));
|
||||
ImGui.Selectable("###console_null", true, ImGuiSelectableFlags.AllowItemOverlap | ImGuiSelectableFlags.SpanAllColumns);
|
||||
|
||||
ImGui.Selectable("###consolenull", true, ImGuiSelectableFlags.AllowItemOverlap | ImGuiSelectableFlags.SpanAllColumns);
|
||||
// This must be after ImGui.Selectable, it uses ImGui.IsItem... functions
|
||||
this.HandleCopyMode(i, line);
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
ImGui.PopStyleColor(3);
|
||||
|
|
@ -364,12 +253,12 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
ImGui.SetNextItemWidth(ImGui.GetWindowSize().X - 80);
|
||||
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - (80.0f * ImGuiHelpers.GlobalScale) - (ImGui.GetStyle().ItemSpacing.X * ImGuiHelpers.GlobalScale));
|
||||
|
||||
var getFocus = false;
|
||||
unsafe
|
||||
{
|
||||
if (ImGui.InputText("##commandbox", ref this.commandText, 255, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CallbackCompletion | ImGuiInputTextFlags.CallbackHistory, this.CommandInputCallback))
|
||||
if (ImGui.InputText("##command_box", ref this.commandText, 255, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CallbackCompletion | ImGuiInputTextFlags.CallbackHistory, this.CommandInputCallback))
|
||||
{
|
||||
this.ProcessCommand();
|
||||
getFocus = true;
|
||||
|
|
@ -385,16 +274,279 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
if (hadColor)
|
||||
ImGui.PopStyleColor();
|
||||
|
||||
if (ImGui.Button("Send"))
|
||||
if (ImGui.Button("Send", ImGuiHelpers.ScaledVector2(80.0f, 23.0f)))
|
||||
{
|
||||
this.ProcessCommand();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleCopyMode(int i, LogEntry line)
|
||||
{
|
||||
var selectionChanged = false;
|
||||
|
||||
// If copyStart is -1, it means a drag has not been started yet, let's start one, and select the starting spot.
|
||||
if (this.copyMode && this.copyStart == -1 && ImGui.IsItemClicked())
|
||||
{
|
||||
this.copyStart = i;
|
||||
line.SelectedForCopy = !line.SelectedForCopy;
|
||||
|
||||
selectionChanged = true;
|
||||
}
|
||||
|
||||
// Update the selected range when dragging over entries
|
||||
if (this.copyMode && this.copyStart != -1 && ImGui.IsItemHovered() && ImGui.IsMouseDragging(ImGuiMouseButton.Left))
|
||||
{
|
||||
if (!line.SelectedForCopy)
|
||||
{
|
||||
foreach (var index in Enumerable.Range(0, this.FilteredLogEntries.Count))
|
||||
{
|
||||
if (this.copyStart < i)
|
||||
{
|
||||
this.FilteredLogEntries[index].SelectedForCopy = index >= this.copyStart && index <= i;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.FilteredLogEntries[index].SelectedForCopy = index >= i && index <= this.copyStart;
|
||||
}
|
||||
}
|
||||
|
||||
selectionChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Finish the drag, we should have already marked all dragged entries as selected by now.
|
||||
if (this.copyMode && this.copyStart != -1 && ImGui.IsItemHovered() && ImGui.IsMouseReleased(ImGuiMouseButton.Left))
|
||||
{
|
||||
this.copyStart = -1;
|
||||
}
|
||||
|
||||
if (selectionChanged)
|
||||
{
|
||||
var allSelectedLines = this.FilteredLogEntries
|
||||
.Where(entry => entry.SelectedForCopy)
|
||||
.Select(entry => $"{line.TimeStamp:HH:mm:ss.fff} {this.GetTextForLogEventLevel(entry.Level)} | {entry.Line}");
|
||||
|
||||
ImGui.SetClipboardText(string.Join("\n", allSelectedLines));
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawOptionsToolbar()
|
||||
{
|
||||
var configuration = Service<DalamudConfiguration>.Get();
|
||||
|
||||
ImGui.PushItemWidth(150.0f * ImGuiHelpers.GlobalScale);
|
||||
if (ImGui.BeginCombo("##log_level", $"{EntryPoint.LogLevelSwitch.MinimumLevel}+"))
|
||||
{
|
||||
foreach (var value in Enum.GetValues<LogEventLevel>())
|
||||
{
|
||||
if (ImGui.Selectable(value.ToString(), value == EntryPoint.LogLevelSwitch.MinimumLevel))
|
||||
{
|
||||
EntryPoint.LogLevelSwitch.MinimumLevel = value;
|
||||
configuration.LogLevel = value;
|
||||
configuration.QueueSave();
|
||||
this.Refilter();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndCombo();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
this.autoScroll = configuration.LogAutoScroll;
|
||||
if (this.DrawToggleButtonWithTooltip("auto_scroll", "Auto-scroll", FontAwesomeIcon.Sync, ref this.autoScroll))
|
||||
{
|
||||
configuration.LogAutoScroll = !configuration.LogAutoScroll;
|
||||
configuration.QueueSave();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
this.autoOpen = configuration.LogOpenAtStartup;
|
||||
if (this.DrawToggleButtonWithTooltip("auto_open", "Open at startup", FontAwesomeIcon.WindowRestore, ref this.autoOpen))
|
||||
{
|
||||
configuration.LogOpenAtStartup = !configuration.LogOpenAtStartup;
|
||||
configuration.QueueSave();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (this.DrawToggleButtonWithTooltip("show_filters", "Show filter toolbar", FontAwesomeIcon.Search, ref this.showFilterToolbar))
|
||||
{
|
||||
this.showFilterToolbar = !this.showFilterToolbar;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (this.DrawToggleButtonWithTooltip("show_uncaught_exceptions", "Show uncaught exception while filtering", FontAwesomeIcon.Bug, ref this.filterShowUncaughtExceptions))
|
||||
{
|
||||
this.filterShowUncaughtExceptions = !this.filterShowUncaughtExceptions;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGuiComponents.IconButton("clear_log", FontAwesomeIcon.Trash))
|
||||
{
|
||||
this.clearLog = true;
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered()) ImGui.SetTooltip("Clear Log");
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (this.DrawToggleButtonWithTooltip("copy_mode", "Enable Copy Mode\nRight-click to copy entire log", FontAwesomeIcon.Copy, ref this.copyMode))
|
||||
{
|
||||
this.copyMode = !this.copyMode;
|
||||
|
||||
if (!this.copyMode)
|
||||
{
|
||||
foreach (var entry in this.FilteredLogEntries)
|
||||
{
|
||||
entry.SelectedForCopy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) this.copyLog = true;
|
||||
|
||||
ImGui.SameLine();
|
||||
if (this.killGameArmed)
|
||||
{
|
||||
if (ImGuiComponents.IconButton(FontAwesomeIcon.ExclamationTriangle))
|
||||
Process.GetCurrentProcess().Kill();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ImGuiComponents.IconButton(FontAwesomeIcon.Stop))
|
||||
this.killGameArmed = true;
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered()) ImGui.SetTooltip("Kill game");
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - (200.0f * ImGuiHelpers.GlobalScale));
|
||||
ImGui.PushItemWidth(200.0f * ImGuiHelpers.GlobalScale);
|
||||
if (ImGui.InputTextWithHint("##global_filter", "regex global filter", ref this.textFilter, 2048, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll))
|
||||
{
|
||||
this.Refilter();
|
||||
}
|
||||
|
||||
if (ImGui.IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
this.Refilter();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawFilterToolbar()
|
||||
{
|
||||
if (!this.showFilterToolbar) return;
|
||||
|
||||
PluginFilterEntry? removalEntry = null;
|
||||
if (ImGui.BeginTable("plugin_filter_entries", 4, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInnerV))
|
||||
{
|
||||
ImGui.TableSetupColumn("##remove_button", ImGuiTableColumnFlags.WidthFixed, 25.0f * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TableSetupColumn("##source_name", ImGuiTableColumnFlags.WidthFixed, 150.0f * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TableSetupColumn("##log_level", ImGuiTableColumnFlags.WidthFixed, 150.0f * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TableSetupColumn("##filter_text", ImGuiTableColumnFlags.WidthStretch);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if (ImGuiComponents.IconButton("add_entry", FontAwesomeIcon.Plus))
|
||||
{
|
||||
if (this.pluginFilters.All(entry => entry.Source != this.selectedSource))
|
||||
{
|
||||
this.pluginFilters.Add(new PluginFilterEntry
|
||||
{
|
||||
Source = this.selectedSource,
|
||||
Filter = string.Empty,
|
||||
Level = LogEventLevel.Debug,
|
||||
});
|
||||
}
|
||||
|
||||
this.Refilter();
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X);
|
||||
if (ImGui.BeginCombo("##Sources", this.selectedSource))
|
||||
{
|
||||
var sourceNames = Service<PluginManager>.Get().InstalledPlugins
|
||||
.Select(p => p.Manifest.InternalName)
|
||||
.OrderBy(s => s)
|
||||
.Prepend("DalamudInternal")
|
||||
.ToList();
|
||||
|
||||
foreach (var selectable in sourceNames)
|
||||
{
|
||||
if (ImGui.Selectable(selectable, this.selectedSource == selectable))
|
||||
{
|
||||
this.selectedSource = selectable;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndCombo();
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
foreach (var entry in this.pluginFilters)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
if (ImGuiComponents.IconButton($"remove{entry.Source}", FontAwesomeIcon.Trash))
|
||||
{
|
||||
removalEntry = entry;
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.Text(entry.Source);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
|
||||
if (ImGui.BeginCombo($"##levels{entry.Source}", $"{entry.Level}+"))
|
||||
{
|
||||
foreach (var value in Enum.GetValues<LogEventLevel>())
|
||||
{
|
||||
if (ImGui.Selectable(value.ToString(), value == entry.Level))
|
||||
{
|
||||
entry.Level = value;
|
||||
this.Refilter();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndCombo();
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
|
||||
var entryFilter = entry.Filter;
|
||||
if (ImGui.InputTextWithHint($"##filter{entry.Source}", $"{entry.Source} regex filter", ref entryFilter, 2048, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll))
|
||||
{
|
||||
entry.Filter = entryFilter;
|
||||
this.Refilter();
|
||||
}
|
||||
|
||||
if (ImGui.IsItemDeactivatedAfterEdit()) this.Refilter();
|
||||
}
|
||||
|
||||
ImGui.EndTable();
|
||||
}
|
||||
|
||||
if (removalEntry is { } toRemove)
|
||||
{
|
||||
this.pluginFilters.Remove(toRemove);
|
||||
this.Refilter();
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessCommand()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (this.commandText is['/', ..])
|
||||
{
|
||||
this.commandText = this.commandText[1..];
|
||||
}
|
||||
|
||||
this.historyPos = -1;
|
||||
for (var i = this.history.Count - 1; i >= 0; i--)
|
||||
{
|
||||
|
|
@ -407,7 +559,7 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
|
||||
this.history.Add(this.commandText);
|
||||
|
||||
if (this.commandText == "clear" || this.commandText == "cls")
|
||||
if (this.commandText is "clear" or "cls")
|
||||
{
|
||||
this.Clear();
|
||||
return;
|
||||
|
|
@ -444,7 +596,9 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
|
||||
// TODO: Improve this, add partial completion
|
||||
// https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L6443-L6484
|
||||
var candidates = Service<CommandManager>.Get().Commands.Where(x => x.Key.Contains("/" + words[0])).ToList();
|
||||
var candidates = Service<CommandManager>.Get().Commands
|
||||
.Where(x => x.Key.Contains("/" + words[0]))
|
||||
.ToList();
|
||||
if (candidates.Count > 0)
|
||||
{
|
||||
ptr.DeleteChars(0, ptr.BufTextLen);
|
||||
|
|
@ -452,6 +606,7 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
}
|
||||
|
||||
break;
|
||||
|
||||
case ImGuiInputTextFlags.CallbackHistory:
|
||||
var prevPos = this.historyPos;
|
||||
|
||||
|
|
@ -501,45 +656,63 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
HasException = logEvent.Exception != null,
|
||||
};
|
||||
|
||||
if (logEvent.Properties.TryGetValue("SourceContext", out var sourceProp) &&
|
||||
sourceProp is ScalarValue { Value: string value })
|
||||
// TODO (v9): Remove SourceContext property check.
|
||||
if (logEvent.Properties.ContainsKey("Dalamud.ModuleName"))
|
||||
{
|
||||
entry.Source = value;
|
||||
entry.Source = "DalamudInternal";
|
||||
}
|
||||
else if ((logEvent.Properties.TryGetValue("Dalamud.PluginName", out var sourceProp) ||
|
||||
logEvent.Properties.TryGetValue("SourceContext", out sourceProp)) &&
|
||||
sourceProp is ScalarValue { Value: string sourceValue })
|
||||
{
|
||||
entry.Source = sourceValue;
|
||||
}
|
||||
|
||||
this.logText.Add(entry);
|
||||
|
||||
if (!this.isFiltered)
|
||||
return;
|
||||
|
||||
if (this.IsFilterApplicable(entry))
|
||||
this.filteredLogText.Add(entry);
|
||||
this.FilteredLogEntries.Add(entry);
|
||||
}
|
||||
|
||||
private bool IsFilterApplicable(LogEntry entry)
|
||||
{
|
||||
if (this.levelFilter > 0 && ((this.levelFilter >> (int)entry.Level) & 1) == 0)
|
||||
// If this entry is below a newly set minimum level, fail it
|
||||
if (EntryPoint.LogLevelSwitch.MinimumLevel > entry.Level)
|
||||
return false;
|
||||
|
||||
|
||||
// Show exceptions that weren't properly tagged with a Source (generally meaning they were uncaught)
|
||||
// After log levels because uncaught exceptions should *never* fall below Error.
|
||||
if (this.filterShowUncaughtExceptions && entry.HasException && entry.Source == null)
|
||||
return true;
|
||||
|
||||
if (this.sourceFilters.Count > 0 && !this.sourceFilters.Contains(entry.Source))
|
||||
return false;
|
||||
// If we have a global filter, check that first
|
||||
if (!this.textFilter.IsNullOrEmpty())
|
||||
{
|
||||
// Someone will definitely try to just text filter a source without using the actual filters, should allow that.
|
||||
var matchesSource = entry.Source is not null && Regex.IsMatch(entry.Source, this.textFilter, RegexOptions.IgnoreCase);
|
||||
var matchesContent = Regex.IsMatch(entry.Line, this.textFilter, RegexOptions.IgnoreCase);
|
||||
|
||||
if (!string.IsNullOrEmpty(this.textFilter) && !entry.Line.Contains(this.textFilter))
|
||||
return false;
|
||||
return matchesSource || matchesContent;
|
||||
}
|
||||
|
||||
return true;
|
||||
// If this entry has a filter, check the filter
|
||||
if (this.pluginFilters.FirstOrDefault(filter => string.Equals(filter.Source, entry.Source, StringComparison.InvariantCultureIgnoreCase)) is { } filterEntry)
|
||||
{
|
||||
var allowedLevel = filterEntry.Level <= entry.Level;
|
||||
var matchesContent = filterEntry.Filter.IsNullOrEmpty() || Regex.IsMatch(entry.Line, filterEntry.Filter, RegexOptions.IgnoreCase);
|
||||
|
||||
return allowedLevel && matchesContent;
|
||||
}
|
||||
|
||||
// else we couldn't find a filter for this entry, if we have any filters, we need to block this entry.
|
||||
return !this.pluginFilters.Any();
|
||||
}
|
||||
|
||||
private void Refilter()
|
||||
{
|
||||
lock (this.renderLock)
|
||||
{
|
||||
this.filteredLogText = this.logText.Where(this.IsFilterApplicable).ToList();
|
||||
this.FilteredLogEntries = this.logText.Where(this.IsFilterApplicable).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -570,18 +743,51 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
this.HandleLogLine(logEvent.Line, logEvent.LogEvent);
|
||||
}
|
||||
|
||||
private bool DrawToggleButtonWithTooltip(string buttonId, string tooltip, FontAwesomeIcon icon, ref bool enabledState)
|
||||
{
|
||||
var result = false;
|
||||
|
||||
var buttonEnabled = enabledState;
|
||||
if (buttonEnabled) ImGui.PushStyleColor(ImGuiCol.Button, ImGuiColors.HealerGreen with { W = 0.25f });
|
||||
if (ImGuiComponents.IconButton(buttonId, icon))
|
||||
{
|
||||
result = true;
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered()) ImGui.SetTooltip(tooltip);
|
||||
|
||||
if (buttonEnabled) ImGui.PopStyleColor();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private class LogEntry
|
||||
{
|
||||
public string Line { get; set; }
|
||||
public string Line { get; init; } = string.Empty;
|
||||
|
||||
public LogEventLevel Level { get; set; }
|
||||
public LogEventLevel Level { get; init; }
|
||||
|
||||
public DateTimeOffset TimeStamp { get; set; }
|
||||
public DateTimeOffset TimeStamp { get; init; }
|
||||
|
||||
public bool IsMultiline { get; set; }
|
||||
public bool IsMultiline { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the system responsible for generating this log entry. Generally will be a plugin's
|
||||
/// InternalName.
|
||||
/// </summary>
|
||||
public string? Source { get; set; }
|
||||
|
||||
public bool SelectedForCopy { get; set; }
|
||||
|
||||
public bool HasException { get; set; }
|
||||
public bool HasException { get; init; }
|
||||
}
|
||||
|
||||
private class PluginFilterEntry
|
||||
{
|
||||
public string Source { get; init; } = string.Empty;
|
||||
|
||||
public string Filter { get; set; } = string.Empty;
|
||||
|
||||
public LogEventLevel Level { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,163 +0,0 @@
|
|||
// ReSharper disable InconsistentNaming // Naming is suppressed so we can replace '_' with ' '
|
||||
namespace Dalamud.Interface.Internal.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Enum representing a DataKind for the Data Window.
|
||||
/// </summary>
|
||||
internal enum DataKind
|
||||
{
|
||||
/// <summary>
|
||||
/// Server Opcode Display.
|
||||
/// </summary>
|
||||
Server_OpCode,
|
||||
|
||||
/// <summary>
|
||||
/// Address.
|
||||
/// </summary>
|
||||
Address,
|
||||
|
||||
/// <summary>
|
||||
/// Object Table.
|
||||
/// </summary>
|
||||
Object_Table,
|
||||
|
||||
/// <summary>
|
||||
/// Fate Table.
|
||||
/// </summary>
|
||||
Fate_Table,
|
||||
|
||||
/// <summary>
|
||||
/// SE Font Test.
|
||||
/// </summary>
|
||||
SE_Font_Test,
|
||||
|
||||
/// <summary>
|
||||
/// FontAwesome Test.
|
||||
/// </summary>
|
||||
FontAwesome_Test,
|
||||
|
||||
/// <summary>
|
||||
/// Party List.
|
||||
/// </summary>
|
||||
Party_List,
|
||||
|
||||
/// <summary>
|
||||
/// Buddy List.
|
||||
/// </summary>
|
||||
Buddy_List,
|
||||
|
||||
/// <summary>
|
||||
/// Plugin IPC Test.
|
||||
/// </summary>
|
||||
Plugin_IPC,
|
||||
|
||||
/// <summary>
|
||||
/// Player Condition.
|
||||
/// </summary>
|
||||
Condition,
|
||||
|
||||
/// <summary>
|
||||
/// Gauge.
|
||||
/// </summary>
|
||||
Gauge,
|
||||
|
||||
/// <summary>
|
||||
/// Command.
|
||||
/// </summary>
|
||||
Command,
|
||||
|
||||
/// <summary>
|
||||
/// Addon.
|
||||
/// </summary>
|
||||
Addon,
|
||||
|
||||
/// <summary>
|
||||
/// Addon Inspector.
|
||||
/// </summary>
|
||||
Addon_Inspector,
|
||||
|
||||
/// <summary>
|
||||
/// AtkArrayData Browser.
|
||||
/// </summary>
|
||||
AtkArrayData_Browser,
|
||||
|
||||
/// <summary>
|
||||
/// StartInfo.
|
||||
/// </summary>
|
||||
StartInfo,
|
||||
|
||||
/// <summary>
|
||||
/// Target.
|
||||
/// </summary>
|
||||
Target,
|
||||
|
||||
/// <summary>
|
||||
/// Toast.
|
||||
/// </summary>
|
||||
Toast,
|
||||
|
||||
/// <summary>
|
||||
/// Fly Text.
|
||||
/// </summary>
|
||||
FlyText,
|
||||
|
||||
/// <summary>
|
||||
/// ImGui.
|
||||
/// </summary>
|
||||
ImGui,
|
||||
|
||||
/// <summary>
|
||||
/// Tex.
|
||||
/// </summary>
|
||||
Tex,
|
||||
|
||||
/// <summary>
|
||||
/// KeyState.
|
||||
/// </summary>
|
||||
KeyState,
|
||||
|
||||
/// <summary>
|
||||
/// GamePad.
|
||||
/// </summary>
|
||||
Gamepad,
|
||||
|
||||
/// <summary>
|
||||
/// Configuration.
|
||||
/// </summary>
|
||||
Configuration,
|
||||
|
||||
/// <summary>
|
||||
/// Task Scheduler.
|
||||
/// </summary>
|
||||
TaskSched,
|
||||
|
||||
/// <summary>
|
||||
/// Hook.
|
||||
/// </summary>
|
||||
Hook,
|
||||
|
||||
/// <summary>
|
||||
/// Aetherytes.
|
||||
/// </summary>
|
||||
Aetherytes,
|
||||
|
||||
/// <summary>
|
||||
/// DTR Bar.
|
||||
/// </summary>
|
||||
Dtr_Bar,
|
||||
|
||||
/// <summary>
|
||||
/// UIColor.
|
||||
/// </summary>
|
||||
UIColor,
|
||||
|
||||
/// <summary>
|
||||
/// Data Share.
|
||||
/// </summary>
|
||||
Data_Share,
|
||||
|
||||
/// <summary>
|
||||
/// Network Monitor.
|
||||
/// </summary>
|
||||
Network_Monitor,
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
|
|
@ -53,26 +52,24 @@ internal class DataWindow : Window
|
|||
new NetworkMonitorWidget(),
|
||||
};
|
||||
|
||||
private readonly Dictionary<DataKind, string> dataKindNames = new();
|
||||
private readonly IOrderedEnumerable<IDataWindowWidget> orderedModules;
|
||||
|
||||
private bool isExcept;
|
||||
private DataKind currentKind;
|
||||
|
||||
private bool selectionCollapsed;
|
||||
private IDataWindowWidget currentWidget;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataWindow"/> class.
|
||||
/// </summary>
|
||||
public DataWindow()
|
||||
: base("Dalamud Data")
|
||||
: base("Dalamud Data", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)
|
||||
{
|
||||
this.Size = new Vector2(500, 500);
|
||||
this.Size = new Vector2(400, 300);
|
||||
this.SizeCondition = ImGuiCond.FirstUseEver;
|
||||
|
||||
this.RespectCloseHotkey = false;
|
||||
|
||||
foreach (var dataKind in Enum.GetValues<DataKind>())
|
||||
{
|
||||
this.dataKindNames[dataKind] = dataKind.ToString().Replace("_", " ");
|
||||
}
|
||||
this.orderedModules = this.modules.OrderBy(module => module.DisplayName);
|
||||
this.currentWidget = this.orderedModules.First();
|
||||
|
||||
this.Load();
|
||||
}
|
||||
|
|
@ -96,24 +93,9 @@ internal class DataWindow : Window
|
|||
if (string.IsNullOrEmpty(dataKind))
|
||||
return;
|
||||
|
||||
dataKind = dataKind switch
|
||||
if (this.modules.FirstOrDefault(module => module.IsWidgetCommand(dataKind)) is { } targetModule)
|
||||
{
|
||||
"ai" => "Addon Inspector",
|
||||
"at" => "Object Table", // Actor Table
|
||||
"ot" => "Object Table",
|
||||
"uic" => "UIColor",
|
||||
_ => dataKind,
|
||||
};
|
||||
|
||||
dataKind = dataKind.Replace(" ", string.Empty).ToLower();
|
||||
|
||||
var matched = Enum
|
||||
.GetValues<DataKind>()
|
||||
.FirstOrDefault(kind => Enum.GetName(kind)?.Replace("_", string.Empty).ToLower() == dataKind);
|
||||
|
||||
if (matched != default)
|
||||
{
|
||||
this.currentKind = matched;
|
||||
this.currentWidget = targetModule;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -126,59 +108,113 @@ internal class DataWindow : Window
|
|||
/// </summary>
|
||||
public override void Draw()
|
||||
{
|
||||
if (ImGuiComponents.IconButton("forceReload", FontAwesomeIcon.Sync)) this.Load();
|
||||
if (ImGui.IsItemHovered()) ImGui.SetTooltip("Force Reload");
|
||||
ImGui.SameLine();
|
||||
var copy = ImGuiComponents.IconButton("copyAll", FontAwesomeIcon.ClipboardList);
|
||||
if (ImGui.IsItemHovered()) ImGui.SetTooltip("Copy All");
|
||||
ImGui.SameLine();
|
||||
|
||||
ImGui.SetNextItemWidth(275.0f * ImGuiHelpers.GlobalScale);
|
||||
if (ImGui.BeginCombo("Data Kind", this.dataKindNames[this.currentKind]))
|
||||
// Only draw the widget contents if the selection pane is collapsed.
|
||||
if (this.selectionCollapsed)
|
||||
{
|
||||
foreach (var module in this.modules.OrderBy(module => this.dataKindNames[module.DataKind]))
|
||||
this.DrawContents();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImGui.BeginTable("XlData_Table", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.Resizable))
|
||||
{
|
||||
ImGui.TableSetupColumn("##SelectionColumn", ImGuiTableColumnFlags.WidthFixed, 200.0f * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TableSetupColumn("##ContentsColumn", ImGuiTableColumnFlags.WidthStretch);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
this.DrawSelection();
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
this.DrawContents();
|
||||
|
||||
ImGui.EndTable();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSelection()
|
||||
{
|
||||
if (ImGui.BeginChild("XlData_SelectionPane", ImGui.GetContentRegionAvail()))
|
||||
{
|
||||
if (ImGui.BeginListBox("WidgetSelectionListbox", ImGui.GetContentRegionAvail()))
|
||||
{
|
||||
if (ImGui.Selectable(this.dataKindNames[module.DataKind], this.currentKind == module.DataKind))
|
||||
foreach (var widget in this.orderedModules)
|
||||
{
|
||||
this.currentKind = module.DataKind;
|
||||
if (ImGui.Selectable(widget.DisplayName, this.currentWidget == widget))
|
||||
{
|
||||
this.currentWidget = widget;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndListBox();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndChild();
|
||||
}
|
||||
|
||||
private void DrawContents()
|
||||
{
|
||||
if (ImGui.BeginChild("XlData_ContentsPane", ImGui.GetContentRegionAvail()))
|
||||
{
|
||||
if (ImGuiComponents.IconButton("collapse-expand", this.selectionCollapsed ? FontAwesomeIcon.ArrowRight : FontAwesomeIcon.ArrowLeft))
|
||||
{
|
||||
this.selectionCollapsed = !this.selectionCollapsed;
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.SetTooltip($"{(this.selectionCollapsed ? "Expand" : "Collapse")} selection pane");
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGuiComponents.IconButton("forceReload", FontAwesomeIcon.Sync))
|
||||
{
|
||||
this.Load();
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.SetTooltip("Force Reload");
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
var copy = ImGuiComponents.IconButton("copyAll", FontAwesomeIcon.ClipboardList);
|
||||
|
||||
ImGuiHelpers.ScaledDummy(10.0f);
|
||||
|
||||
if (ImGui.BeginChild("XlData_WidgetContents", ImGui.GetContentRegionAvail()))
|
||||
{
|
||||
if (copy)
|
||||
ImGui.LogToClipboard();
|
||||
|
||||
try
|
||||
{
|
||||
if (this.currentWidget is { Ready: true })
|
||||
{
|
||||
this.currentWidget.Draw();
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted("Data not ready.");
|
||||
}
|
||||
|
||||
this.isExcept = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!this.isExcept)
|
||||
{
|
||||
Log.Error(ex, "Could not draw data");
|
||||
}
|
||||
|
||||
this.isExcept = true;
|
||||
|
||||
ImGui.TextUnformatted(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndCombo();
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(10.0f);
|
||||
|
||||
ImGui.BeginChild("scrolling", Vector2.Zero, false, ImGuiWindowFlags.HorizontalScrollbar);
|
||||
|
||||
if (copy)
|
||||
ImGui.LogToClipboard();
|
||||
|
||||
try
|
||||
{
|
||||
var selectedWidget = this.modules.FirstOrDefault(dataWindowWidget => dataWindowWidget.DataKind == this.currentKind);
|
||||
|
||||
if (selectedWidget is { Ready: true })
|
||||
{
|
||||
selectedWidget.Draw();
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted("Data not ready.");
|
||||
}
|
||||
|
||||
this.isExcept = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!this.isExcept)
|
||||
{
|
||||
Log.Error(ex, "Could not draw data");
|
||||
}
|
||||
|
||||
this.isExcept = true;
|
||||
|
||||
ImGui.TextUnformatted(ex.ToString());
|
||||
ImGui.EndChild();
|
||||
}
|
||||
|
||||
ImGui.EndChild();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
namespace Dalamud.Interface.Internal.Windows;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Dalamud.Interface.Internal.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Class representing a date window entry.
|
||||
|
|
@ -6,9 +9,14 @@
|
|||
internal interface IDataWindowWidget
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the Data Kind for this data window module.
|
||||
/// Gets the command strings that can be used to open the data window directly to this module.
|
||||
/// </summary>
|
||||
DataKind DataKind { get; init; }
|
||||
string[]? CommandShortcuts { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display name for this module.
|
||||
/// </summary>
|
||||
string DisplayName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this data window module is ready.
|
||||
|
|
@ -24,4 +32,11 @@ internal interface IDataWindowWidget
|
|||
/// Draws this data window module.
|
||||
/// </summary>
|
||||
void Draw();
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to check if this widget should be activated by the input command.
|
||||
/// </summary>
|
||||
/// <param name="command">The command being run.</param>
|
||||
/// <returns>true if this module should be activated by the input command.</returns>
|
||||
bool IsWidgetCommand(string command) => this.CommandShortcuts?.Any(shortcut => string.Equals(shortcut, command, StringComparison.InvariantCultureIgnoreCase)) ?? false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ internal class AddonInspectorWidget : IDataWindowWidget
|
|||
private UiDebug? addonInspector;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Addon_Inspector;
|
||||
public string[]? CommandShortcuts { get; init; } = { "ai", "addoninspector" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Addon Inspector";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -15,7 +15,10 @@ internal unsafe class AddonWidget : IDataWindowWidget
|
|||
private nint findAgentInterfacePtr;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Addon;
|
||||
public string DisplayName { get; init; } = "Addon";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string[]? CommandShortcuts { get; init; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ internal class AddressesWidget : IDataWindowWidget
|
|||
private nint sigResult = nint.Zero;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Address;
|
||||
public string[]? CommandShortcuts { get; init; } = { "address" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Addresses";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -9,10 +9,13 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
|||
internal class AetherytesWidget : IDataWindowWidget
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Aetherytes;
|
||||
public bool Ready { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
public string[]? CommandShortcuts { get; init; } = { "aetherytes" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Aetherytes";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Load()
|
||||
|
|
|
|||
|
|
@ -11,10 +11,13 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
|||
internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.AtkArrayData_Browser;
|
||||
public bool Ready { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
public string[]? CommandShortcuts { get; init; } = { "atkarray" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Atk Array Data";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Load()
|
||||
|
|
|
|||
|
|
@ -12,10 +12,13 @@ internal class BuddyListWidget : IDataWindowWidget
|
|||
private bool resolveGameData;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Buddy_List;
|
||||
public bool Ready { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
public string[]? CommandShortcuts { get; init; } = { "buddy", "buddylist" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Buddy List";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Load()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
|||
internal class CommandWidget : IDataWindowWidget
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Command;
|
||||
public string[]? CommandShortcuts { get; init; } = { "command" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Command";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -9,10 +9,13 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
|||
internal class ConditionWidget : IDataWindowWidget
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Condition;
|
||||
public bool Ready { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
public string[]? CommandShortcuts { get; init; } = { "condition" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Condition";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Load()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
|||
internal class ConfigurationWidget : IDataWindowWidget
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Configuration;
|
||||
public string[]? CommandShortcuts { get; init; } = { "config", "configuration" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Configuration";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
|||
internal class DataShareWidget : IDataWindowWidget
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Data_Share;
|
||||
public string[]? CommandShortcuts { get; init; } = { "datashare" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Data Share";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ internal class DtrBarWidget : IDataWindowWidget
|
|||
private DtrBarEntry? dtrTest3;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Dtr_Bar;
|
||||
public string[]? CommandShortcuts { get; init; } = { "dtr", "dtrbar" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "DTR Bar";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@ internal class FateTableWidget : IDataWindowWidget
|
|||
private bool resolveGameData;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Fate_Table;
|
||||
public string[]? CommandShortcuts { get; init; } = { "fate", "fatetable" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Fate Table";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ internal class FlyTextWidget : IDataWindowWidget
|
|||
private Vector4 flyColor = new(1, 0, 0, 1);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.FlyText;
|
||||
public string[]? CommandShortcuts { get; init; } = { "flytext" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Fly Text";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@ internal class FontAwesomeTestWidget : IDataWindowWidget
|
|||
private bool iconSearchChanged = true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.FontAwesome_Test;
|
||||
public string[]? CommandShortcuts { get; init; } = { "fa", "fatest", "fontawesome" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Font Awesome Test";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
|||
internal class GamepadWidget : IDataWindowWidget
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Gamepad;
|
||||
public string[]? CommandShortcuts { get; init; } = { "gamepad", "controller" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Gamepad";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
|||
internal class GaugeWidget : IDataWindowWidget
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Gauge;
|
||||
public string[]? CommandShortcuts { get; init; } = { "gauge", "jobgauge", "job" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Job Gauge";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -22,8 +22,11 @@ internal class HookWidget : IDataWindowWidget
|
|||
NativeFunctions.MessageBoxType type);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Hook;
|
||||
public string DisplayName { get; init; } = "Hook";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string[]? CommandShortcuts { get; init; } = { "hook" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
|||
internal class ImGuiWidget : IDataWindowWidget
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.ImGui;
|
||||
public string[]? CommandShortcuts { get; init; } = { "imgui" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "ImGui";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
|||
internal class KeyStateWidget : IDataWindowWidget
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.KeyState;
|
||||
public string[]? CommandShortcuts { get; init; } = { "keystate" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "KeyState";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -54,7 +54,10 @@ internal class NetworkMonitorWidget : IDataWindowWidget
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Network_Monitor;
|
||||
public string[]? CommandShortcuts { get; init; } = { "network", "netmon", "networkmonitor" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Network Monitor";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -18,8 +18,11 @@ internal class ObjectTableWidget : IDataWindowWidget
|
|||
private float maxCharaDrawDistance = 20.0f;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Object_Table;
|
||||
public string[]? CommandShortcuts { get; init; } = { "ot", "objecttable" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Object Table";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ internal class PartyListWidget : IDataWindowWidget
|
|||
private bool resolveGameData;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Party_List;
|
||||
public string[]? CommandShortcuts { get; init; } = { "partylist", "party" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Party List";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@ internal class PluginIpcWidget : IDataWindowWidget
|
|||
private string callGateResponse = string.Empty;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Plugin_IPC;
|
||||
public string[]? CommandShortcuts { get; init; } = { "ipc" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Plugin IPC";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
|||
internal class SeFontTestWidget : IDataWindowWidget
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.SE_Font_Test;
|
||||
public string[]? CommandShortcuts { get; init; } = { "sefont", "sefonttest" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "SeFont Test";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ internal class ServerOpcodeWidget : IDataWindowWidget
|
|||
private string? serverOpString;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Server_OpCode;
|
||||
public string[]? CommandShortcuts { get; init; } = { "opcode", "serveropcode" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Server Opcode";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
|||
internal class StartInfoWidget : IDataWindowWidget
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.StartInfo;
|
||||
public string[]? CommandShortcuts { get; init; } = { "startinfo" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Start Info";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ internal class TargetWidget : IDataWindowWidget
|
|||
private bool resolveGameData;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Target;
|
||||
public string[]? CommandShortcuts { get; init; } = { "target" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Target";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ internal class TaskSchedulerWidget : IDataWindowWidget
|
|||
private CancellationTokenSource taskSchedulerCancelSource = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.TaskSched;
|
||||
public string[]? CommandShortcuts { get; init; } = { "tasksched", "taskscheduler" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Task Scheduler";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -28,7 +28,10 @@ internal class TexWidget : IDataWindowWidget
|
|||
private Vector2 inputTexScale = Vector2.Zero;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Tex;
|
||||
public string[]? CommandShortcuts { get; init; } = { "tex", "texture" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Tex";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@ internal class ToastWidget : IDataWindowWidget
|
|||
private bool questToastCheckmark;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Toast;
|
||||
public string[]? CommandShortcuts { get; init; } = { "toast" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Toast";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
|||
internal class UIColorWidget : IDataWindowWidget
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.UIColor;
|
||||
public string[]? CommandShortcuts { get; init; } = { "uicolor" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "UIColor";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ public class ModuleLog
|
|||
{
|
||||
private readonly string moduleName;
|
||||
private readonly ILogger moduleLogger;
|
||||
|
||||
// FIXME (v9): Deprecate this class in favor of using contextualized ILoggers with proper formatting.
|
||||
// We can keep this class around as a Serilog helper, but ModuleLog should no longer be a returned
|
||||
// type, instead returning a (prepared) ILogger appropriately.
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModuleLog"/> class.
|
||||
|
|
@ -20,10 +24,8 @@ public class ModuleLog
|
|||
/// <param name="moduleName">The module name.</param>
|
||||
public ModuleLog(string? moduleName)
|
||||
{
|
||||
// FIXME: Should be namespaced better, e.g. `Dalamud.PluginLoader`, but that becomes a relatively large
|
||||
// change.
|
||||
this.moduleName = moduleName ?? "DalamudInternal";
|
||||
this.moduleLogger = Log.ForContext("SourceContext", this.moduleName);
|
||||
this.moduleLogger = Log.ForContext("Dalamud.ModuleName", this.moduleName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -128,7 +130,8 @@ public class ModuleLog
|
|||
public void Fatal(Exception exception, string messageTemplate, params object[] values)
|
||||
=> this.WriteLog(LogEventLevel.Fatal, messageTemplate, exception, values);
|
||||
|
||||
private void WriteLog(LogEventLevel level, string messageTemplate, Exception? exception = null, params object[] values)
|
||||
private void WriteLog(
|
||||
LogEventLevel level, string messageTemplate, Exception? exception = null, params object[] values)
|
||||
{
|
||||
// FIXME: Eventually, the `pluginName` tag should be removed from here and moved over to the actual log
|
||||
// formatter.
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ public static class PluginLog
|
|||
|
||||
private static ILogger GetPluginLogger(string? pluginName)
|
||||
{
|
||||
return Serilog.Log.ForContext("SourceContext", pluginName ?? string.Empty);
|
||||
return Serilog.Log.ForContext("Dalamud.PluginName", pluginName ?? string.Empty);
|
||||
}
|
||||
|
||||
private static void WriteLog(string? pluginName, LogEventLevel level, string messageTemplate, Exception? exception = null, params object[] values)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 7279a8f3ca6b79490184b05532af509781a89415
|
||||
Subproject commit a593cb163e1c5d33b27d34df4d1ccc57d1e67643
|
||||
Loading…
Add table
Add a link
Reference in a new issue