Merge remote-tracking branch 'origin/master' into v9-rollup

This commit is contained in:
github-actions[bot] 2023-07-27 12:07:53 +00:00
commit 5da34cbc81
91 changed files with 3849 additions and 1089 deletions

View file

@ -0,0 +1,107 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
// ReSharper disable UnusedMember.Local
// ReSharper disable IdentifierTypo
// ReSharper disable InconsistentNaming
namespace Dalamud.Interface.DragDrop;
#pragma warning disable SA1600 // Elements should be documented
/// <summary> Implements interop enums and function calls to interact with external drag and drop. </summary>
internal partial class DragDropManager
{
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("00000122-0000-0000-C000-000000000046")]
[ComImport]
public interface IDropTarget
{
[MethodImpl(MethodImplOptions.InternalCall)]
void DragEnter([MarshalAs(UnmanagedType.Interface), In] IDataObject pDataObj, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In] uint grfKeyState, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.POINTL"), In] POINTL pt, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In, Out] ref uint pdwEffect);
[MethodImpl(MethodImplOptions.InternalCall)]
void DragOver([ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In] uint grfKeyState, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.POINTL"), In] POINTL pt, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In, Out] ref uint pdwEffect);
[MethodImpl(MethodImplOptions.InternalCall)]
void DragLeave();
[MethodImpl(MethodImplOptions.InternalCall)]
void Drop([MarshalAs(UnmanagedType.Interface), In] IDataObject pDataObj, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In] uint grfKeyState, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.POINTL"), In] POINTL pt, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In, Out] ref uint pdwEffect);
}
internal struct POINTL
{
[ComAliasName("Microsoft.VisualStudio.OLE.Interop.LONG")]
public int x;
[ComAliasName("Microsoft.VisualStudio.OLE.Interop.LONG")]
public int y;
}
private static class DragDropInterop
{
[Flags]
public enum ModifierKeys
{
MK_NONE = 0x00,
MK_LBUTTON = 0x01,
MK_RBUTTON = 0x02,
MK_SHIFT = 0x04,
MK_CONTROL = 0x08,
MK_MBUTTON = 0x10,
MK_ALT = 0x20,
}
public enum ClipboardFormat
{
CF_TEXT = 1,
CF_BITMAP = 2,
CF_DIB = 3,
CF_UNICODETEXT = 13,
CF_HDROP = 15,
}
[Flags]
public enum DVAspect
{
DVASPECT_CONTENT = 0x01,
DVASPECT_THUMBNAIL = 0x02,
DVASPECT_ICON = 0x04,
DVASPECT_DOCPRINT = 0x08,
}
[Flags]
public enum TYMED
{
TYMED_NULL = 0x00,
TYMED_HGLOBAL = 0x01,
TYMED_FILE = 0x02,
TYMED_ISTREAM = 0x04,
TYMED_ISTORAGE = 0x08,
TYMED_GDI = 0x10,
TYMED_MFPICT = 0x20,
TYMED_ENHMF = 0x40,
}
[Flags]
public enum DropEffects : uint
{
None = 0x00_0000_00,
Copy = 0x00_0000_01,
Move = 0x00_0000_02,
Link = 0x00_0000_04,
Scroll = 0x80_0000_00,
}
[DllImport("ole32.dll")]
public static extern int RegisterDragDrop(nint hwnd, IDropTarget pDropTarget);
[DllImport("ole32.dll")]
public static extern int RevokeDragDrop(nint hwnd);
[DllImport("shell32.dll")]
public static extern int DragQueryFile(IntPtr hDrop, uint iFile, StringBuilder lpszFile, int cch);
}
}
#pragma warning restore SA1600 // Elements should be documented

View file

@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Dalamud.Interface.Internal;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using ImGuiNET;
using Serilog;
namespace Dalamud.Interface.DragDrop;
/// <summary>
/// A manager that keeps state of external windows drag and drop events,
/// and can be used to create ImGui drag and drop sources and targets for those external events.
/// </summary>
[PluginInterface]
[ServiceManager.EarlyLoadedService]
[ResolveVia<IDragDropManager>]
internal partial class DragDropManager : IDisposable, IDragDropManager, IServiceType
{
private nint windowHandlePtr = nint.Zero;
private int lastDropFrame = -2;
private int lastTooltipFrame = -1;
[ServiceManager.ServiceConstructor]
private DragDropManager()
{
Service<InterfaceManager.InterfaceManagerWithScene>.GetAsync()
.ContinueWith(t =>
{
this.windowHandlePtr = t.Result.Manager.WindowHandlePtr;
this.Enable();
});
}
/// <summary> Gets a value indicating whether external drag and drop is available at all. </summary>
public bool ServiceAvailable { get; private set; }
/// <summary> Gets a value indicating whether a valid external drag and drop is currently active and hovering over any FFXIV-related viewport. </summary>
public bool IsDragging { get; private set; }
/// <summary> Gets a value indicating whether there are any files or directories currently being dragged, or stored from the last drop. </summary>
public bool HasPaths
=> this.Files.Count + this.Directories.Count > 0;
/// <summary> Gets the list of file paths currently being dragged from an external application over any FFXIV-related viewport, or stored from the last drop. </summary>
public IReadOnlyList<string> Files { get; private set; } = Array.Empty<string>();
/// <summary> Gets a set of all extensions available in the paths currently being dragged from an external application over any FFXIV-related viewport or stored from the last drop. </summary>
public IReadOnlySet<string> Extensions { get; private set; } = new HashSet<string>();
/// <summary> Gets the list of directory paths currently being dragged from an external application over any FFXIV-related viewport or stored from the last drop. </summary>
public IReadOnlyList<string> Directories { get; private set; } = Array.Empty<string>();
/// <summary> Enable external drag and drop. </summary>
public void Enable()
{
if (this.ServiceAvailable || this.windowHandlePtr == nint.Zero)
{
return;
}
try
{
var ret = DragDropInterop.RegisterDragDrop(this.windowHandlePtr, this);
Log.Information($"[DragDrop] Registered window 0x{this.windowHandlePtr:X} for external drag and drop operations. ({ret})");
Marshal.ThrowExceptionForHR(ret);
this.ServiceAvailable = true;
}
catch (Exception ex)
{
Log.Error($"Could not create windows drag and drop utility for window 0x{this.windowHandlePtr:X}:\n{ex}");
}
}
/// <summary> Disable external drag and drop. </summary>
public void Disable()
{
if (!this.ServiceAvailable)
{
return;
}
try
{
var ret = DragDropInterop.RevokeDragDrop(this.windowHandlePtr);
Log.Information($"[DragDrop] Disabled external drag and drop operations for window 0x{this.windowHandlePtr:X}. ({ret})");
Marshal.ThrowExceptionForHR(ret);
}
catch (Exception ex)
{
Log.Error($"Could not disable windows drag and drop utility for window 0x{this.windowHandlePtr:X}:\n{ex}");
}
this.ServiceAvailable = false;
}
/// <inheritdoc cref="Disable"/>
public void Dispose()
=> this.Disable();
/// <inheritdoc cref="IDragDropManager.CreateImGuiSource(string, Func{IDragDropManager, bool}, Func{IDragDropManager, bool})"/>
public void CreateImGuiSource(string label, Func<IDragDropManager, bool> validityCheck, Func<IDragDropManager, bool> tooltipBuilder)
{
if (!this.IsDragging && !this.IsDropping())
{
return;
}
if (!validityCheck(this) || !ImGui.BeginDragDropSource(ImGuiDragDropFlags.SourceExtern))
{
return;
}
ImGui.SetDragDropPayload(label, nint.Zero, 0);
if (this.CheckTooltipFrame(out var frame) && tooltipBuilder(this))
{
this.lastTooltipFrame = frame;
}
ImGui.EndDragDropSource();
}
/// <inheritdoc cref="IDragDropManager.CreateImGuiTarget"/>
public bool CreateImGuiTarget(string label, out IReadOnlyList<string> files, out IReadOnlyList<string> directories)
{
files = Array.Empty<string>();
directories = Array.Empty<string>();
if (!this.HasPaths || !ImGui.BeginDragDropTarget())
{
return false;
}
unsafe
{
if (ImGui.AcceptDragDropPayload(label, ImGuiDragDropFlags.AcceptBeforeDelivery).NativePtr != null && this.IsDropping())
{
this.lastDropFrame = -2;
files = this.Files;
directories = this.Directories;
return true;
}
}
ImGui.EndDragDropTarget();
return false;
}
private bool CheckTooltipFrame(out int frame)
{
frame = ImGui.GetFrameCount();
return this.lastTooltipFrame < frame;
}
private bool IsDropping()
{
var frame = ImGui.GetFrameCount();
return this.lastDropFrame == frame || this.lastDropFrame == frame - 1;
}
}

View file

@ -0,0 +1,248 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using Dalamud.Utility;
using ImGuiNET;
using Serilog;
namespace Dalamud.Interface.DragDrop;
/// <summary> Implements the IDropTarget interface to interact with external drag and dropping. </summary>
internal partial class DragDropManager : DragDropManager.IDropTarget
{
private int lastUpdateFrame = -1;
private DragDropInterop.ModifierKeys lastKeyState = DragDropInterop.ModifierKeys.MK_NONE;
/// <summary> Create the drag and drop formats we accept. </summary>
private FORMATETC formatEtc =
new()
{
cfFormat = (short)DragDropInterop.ClipboardFormat.CF_HDROP,
ptd = nint.Zero,
dwAspect = DVASPECT.DVASPECT_CONTENT,
lindex = -1,
tymed = TYMED.TYMED_HGLOBAL,
};
/// <summary>
/// Invoked whenever a drag and drop process drags files into any FFXIV-related viewport.
/// </summary>
/// <param name="pDataObj"> The drag and drop data. </param>
/// <param name="grfKeyState"> The mouse button used to drag as well as key modifiers. </param>
/// <param name="pt"> The global cursor position. </param>
/// <param name="pdwEffect"> Effects that can be used with this drag and drop process. </param>
public void DragEnter(IDataObject pDataObj, uint grfKeyState, POINTL pt, ref uint pdwEffect)
{
this.IsDragging = true;
this.lastKeyState = UpdateIo((DragDropInterop.ModifierKeys)grfKeyState, true);
if (pDataObj.QueryGetData(ref this.formatEtc) != 0)
{
pdwEffect = 0;
}
else
{
pdwEffect &= (uint)DragDropInterop.DropEffects.Copy;
(this.Files, this.Directories) = this.GetPaths(pDataObj);
this.Extensions = this.Files.Select(Path.GetExtension).Where(p => !p.IsNullOrEmpty()).Distinct().ToHashSet();
}
Log.Debug("[DragDrop] Entering external Drag and Drop with {KeyState} at {PtX}, {PtY} and with {N} files.", (DragDropInterop.ModifierKeys)grfKeyState, pt.x, pt.y, this.Files.Count + this.Directories.Count);
}
/// <summary> Invoked every windows update-frame as long as the drag and drop process keeps hovering over an FFXIV-related viewport. </summary>
/// <param name="grfKeyState"> The mouse button used to drag as well as key modifiers. </param>
/// <param name="pt"> The global cursor position. </param>
/// <param name="pdwEffect"> Effects that can be used with this drag and drop process. </param>
/// <remarks> Can be invoked more often than once a XIV frame, so we are keeping track of frames to skip unnecessary updates. </remarks>
public void DragOver(uint grfKeyState, POINTL pt, ref uint pdwEffect)
{
var frame = ImGui.GetFrameCount();
if (frame != this.lastUpdateFrame)
{
this.lastUpdateFrame = frame;
this.lastKeyState = UpdateIo((DragDropInterop.ModifierKeys)grfKeyState, false);
pdwEffect &= (uint)DragDropInterop.DropEffects.Copy;
Log.Verbose("[DragDrop] External Drag and Drop with {KeyState} at {PtX}, {PtY}.", (DragDropInterop.ModifierKeys)grfKeyState, pt.x, pt.y);
}
}
/// <summary> Invoked whenever a drag and drop process that hovered over any FFXIV-related viewport leaves all FFXIV-related viewports. </summary>
public void DragLeave()
{
this.IsDragging = false;
this.Files = Array.Empty<string>();
this.Directories = Array.Empty<string>();
this.Extensions = new HashSet<string>();
MouseDrop(this.lastKeyState);
Log.Debug("[DragDrop] Leaving external Drag and Drop.");
}
/// <summary> Invoked whenever a drag process ends by dropping over any FFXIV-related viewport. </summary>
/// <param name="pDataObj"> The drag and drop data. </param>
/// <param name="grfKeyState"> The mouse button used to drag as well as key modifiers. </param>
/// <param name="pt"> The global cursor position. </param>
/// <param name="pdwEffect"> Effects that can be used with this drag and drop process. </param>
public void Drop(IDataObject pDataObj, uint grfKeyState, POINTL pt, ref uint pdwEffect)
{
MouseDrop(this.lastKeyState);
this.lastDropFrame = ImGui.GetFrameCount();
this.IsDragging = false;
if (this.HasPaths)
{
pdwEffect &= (uint)DragDropInterop.DropEffects.Copy;
}
else
{
pdwEffect = 0;
}
Log.Debug("[DragDrop] Dropping {N} files with {KeyState} at {PtX}, {PtY}.", this.Files.Count + this.Directories.Count, (DragDropInterop.ModifierKeys)grfKeyState, pt.x, pt.y);
}
private static DragDropInterop.ModifierKeys UpdateIo(DragDropInterop.ModifierKeys keys, bool entering)
{
var io = ImGui.GetIO();
void UpdateMouse(int mouseIdx)
{
if (entering)
{
io.MouseDownDuration[mouseIdx] = 1f;
}
io.MouseDown[mouseIdx] = true;
io.AddMouseButtonEvent(mouseIdx, true);
}
if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_LBUTTON))
{
UpdateMouse(0);
}
if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_RBUTTON))
{
UpdateMouse(1);
}
if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_MBUTTON))
{
UpdateMouse(2);
}
if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_CONTROL))
{
io.KeyCtrl = true;
io.AddKeyEvent(ImGuiKey.LeftCtrl, true);
}
else
{
io.KeyCtrl = false;
io.AddKeyEvent(ImGuiKey.LeftCtrl, false);
}
if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_ALT))
{
io.KeyAlt = true;
io.AddKeyEvent(ImGuiKey.LeftAlt, true);
}
else
{
io.KeyAlt = false;
io.AddKeyEvent(ImGuiKey.LeftAlt, false);
}
if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_SHIFT))
{
io.KeyShift = true;
io.AddKeyEvent(ImGuiKey.LeftShift, true);
}
else
{
io.KeyShift = false;
io.AddKeyEvent(ImGuiKey.LeftShift, false);
}
return keys;
}
private static void MouseDrop(DragDropInterop.ModifierKeys keys)
{
var io = ImGui.GetIO();
void UpdateMouse(int mouseIdx)
{
io.AddMouseButtonEvent(mouseIdx, false);
io.MouseDown[mouseIdx] = false;
}
if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_LBUTTON))
{
UpdateMouse(0);
}
if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_RBUTTON))
{
UpdateMouse(1);
}
if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_MBUTTON))
{
UpdateMouse(2);
}
}
private (string[] Files, string[] Directories) GetPaths(IDataObject data)
{
if (!this.IsDragging)
{
return (Array.Empty<string>(), Array.Empty<string>());
}
try
{
data.GetData(ref this.formatEtc, out var stgMedium);
var numFiles = DragDropInterop.DragQueryFile(stgMedium.unionmember, uint.MaxValue, new StringBuilder(), 0);
var files = new string[numFiles];
var sb = new StringBuilder(1024);
var directoryCount = 0;
var fileCount = 0;
for (var i = 0u; i < numFiles; ++i)
{
sb.Clear();
var ret = DragDropInterop.DragQueryFile(stgMedium.unionmember, i, sb, sb.Capacity);
if (ret >= sb.Capacity)
{
sb.Capacity = ret + 1;
ret = DragDropInterop.DragQueryFile(stgMedium.unionmember, i, sb, sb.Capacity);
}
if (ret > 0 && ret < sb.Capacity)
{
var s = sb.ToString();
if (Directory.Exists(s))
{
files[^(++directoryCount)] = s;
}
else
{
files[fileCount++] = s;
}
}
}
var fileArray = fileCount > 0 ? files.Take(fileCount).ToArray() : Array.Empty<string>();
var directoryArray = directoryCount > 0 ? files.TakeLast(directoryCount).Reverse().ToArray() : Array.Empty<string>();
return (fileArray, directoryArray);
}
catch (Exception ex)
{
Log.Error($"Error obtaining data from drag & drop:\n{ex}");
}
return (Array.Empty<string>(), Array.Empty<string>());
}
}

View file

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
namespace Dalamud.Interface.DragDrop;
/// <summary>
/// A service to handle external drag and drop from WinAPI.
/// </summary>
public interface IDragDropManager
{
/// <summary> Gets a value indicating whether Drag and Drop functionality is available at all. </summary>
public bool ServiceAvailable { get; }
/// <summary> Gets a value indicating whether anything is being dragged from an external application and over any of the games viewports. </summary>
public bool IsDragging { get; }
/// <summary> Gets the list of files currently being dragged from an external application over any of the games viewports. </summary>
public IReadOnlyList<string> Files { get; }
/// <summary> Gets the set of file types by extension currently being dragged from an external application over any of the games viewports. </summary>
public IReadOnlySet<string> Extensions { get; }
/// <summary> Gets the list of directories currently being dragged from an external application over any of the games viewports. </summary>
public IReadOnlyList<string> Directories { get; }
/// <summary> Create an ImGui drag & drop source that is active only if anything is being dragged from an external source. </summary>
/// <param name="label"> The label used for the drag & drop payload. </param>
/// <param name="validityCheck">A function returning whether the current status is relevant for this source. Checked before creating the source but only if something is being dragged.</param>
public void CreateImGuiSource(string label, Func<IDragDropManager, bool> validityCheck)
=> this.CreateImGuiSource(label, validityCheck, _ => false);
/// <summary> Create an ImGui drag & drop source that is active only if anything is being dragged from an external source. </summary>
/// <param name="label"> The label used for the drag & drop payload. </param>
/// <param name="validityCheck">A function returning whether the current status is relevant for this source. Checked before creating the source but only if something is being dragged.</param>
/// <param name="tooltipBuilder">Executes ImGui functions to build a tooltip. Should return true if it creates any tooltip and false otherwise. If multiple sources are active, only the first non-empty tooltip type drawn in a frame will be used.</param>
public void CreateImGuiSource(string label, Func<IDragDropManager, bool> validityCheck, Func<IDragDropManager, bool> tooltipBuilder);
/// <summary> Create an ImGui drag & drop target on the last ImGui object. </summary>
/// <param name="label">The label used for the drag & drop payload.</param>
/// <param name="files">On success, contains the list of file paths dropped onto the target.</param>
/// <param name="directories">On success, contains the list of directory paths dropped onto the target.</param>
/// <returns>True if items were dropped onto the target this frame, false otherwise.</returns>
public bool CreateImGuiTarget(string label, out IReadOnlyList<string> files, out IReadOnlyList<string> directories);
}

View file

@ -1147,10 +1147,10 @@ internal class InterfaceManager : IDisposable, IServiceType
var dPadRight = gamepadState.Raw(GamepadButtons.DpadRight) != 0;
var dPadDown = gamepadState.Raw(GamepadButtons.DpadDown) != 0;
var dPadLeft = gamepadState.Raw(GamepadButtons.DpadLeft) != 0;
var leftStickUp = gamepadState.LeftStickUp;
var leftStickRight = gamepadState.LeftStickRight;
var leftStickDown = gamepadState.LeftStickDown;
var leftStickLeft = gamepadState.LeftStickLeft;
var leftStickUp = gamepadState.LeftStick.Y > 0 ? gamepadState.LeftStick.Y / 100f : 0;
var leftStickRight = gamepadState.LeftStick.X > 0 ? gamepadState.LeftStick.X / 100f : 0;
var leftStickDown = gamepadState.LeftStick.Y < 0 ? -gamepadState.LeftStick.Y / 100f : 0;
var leftStickLeft = gamepadState.LeftStick.X < 0 ? -gamepadState.LeftStick.X / 100f : 0;
var l1Button = gamepadState.Raw(GamepadButtons.L1) != 0;
var l2Button = gamepadState.Raw(GamepadButtons.L2) != 0;
var r1Button = gamepadState.Raw(GamepadButtons.R1) != 0;

View file

@ -154,5 +154,10 @@ internal enum DataKind
/// <summary>
/// Data Share.
/// </summary>
DataShare,
Data_Share,
/// <summary>
/// Network Monitor.
/// </summary>
Network_Monitor,
}

View file

@ -48,6 +48,7 @@ internal class DataWindow : Window
new DtrBarWidget(),
new UIColorWidget(),
new DataShareWidget(),
new NetworkMonitorWidget(),
};
private readonly Dictionary<DataKind, string> dataKindNames = new();

View file

@ -9,7 +9,7 @@ namespace Dalamud.Interface.Internal.Windows.Data;
internal class DataShareWidget : IDataWindowWidget
{
/// <inheritdoc/>
public DataKind DataKind { get; init; } = DataKind.DataShare;
public DataKind DataKind { get; init; } = DataKind.Data_Share;
/// <inheritdoc/>
public bool Ready { get; set; }

View file

@ -58,7 +58,7 @@ internal class FateTableWidget : IDataWindowWidget
ImGui.TextUnformatted(fateString);
ImGui.SameLine();
if (ImGui.Button("C"))
if (ImGui.Button($"C##{fate.Address.ToInt64():X}"))
{
ImGui.SetClipboardText(fate.Address.ToString("X"));
}

View file

@ -27,27 +27,6 @@ internal class GamepadWidget : IDataWindowWidget
{
var gamepadState = Service<GamepadState>.Get();
static void DrawHelper(string text, uint mask, Func<GamepadButtons, float> resolve)
{
ImGui.Text($"{text} {mask:X4}");
ImGui.Text($"DPadLeft {resolve(GamepadButtons.DpadLeft)} " +
$"DPadUp {resolve(GamepadButtons.DpadUp)} " +
$"DPadRight {resolve(GamepadButtons.DpadRight)} " +
$"DPadDown {resolve(GamepadButtons.DpadDown)} ");
ImGui.Text($"West {resolve(GamepadButtons.West)} " +
$"North {resolve(GamepadButtons.North)} " +
$"East {resolve(GamepadButtons.East)} " +
$"South {resolve(GamepadButtons.South)} ");
ImGui.Text($"L1 {resolve(GamepadButtons.L1)} " +
$"L2 {resolve(GamepadButtons.L2)} " +
$"R1 {resolve(GamepadButtons.R1)} " +
$"R2 {resolve(GamepadButtons.R2)} ");
ImGui.Text($"Select {resolve(GamepadButtons.Select)} " +
$"Start {resolve(GamepadButtons.Start)} " +
$"L3 {resolve(GamepadButtons.L3)} " +
$"R3 {resolve(GamepadButtons.R3)} ");
}
ImGui.Text($"GamepadInput 0x{gamepadState.GamepadInputAddress.ToInt64():X}");
#if DEBUG
@ -58,29 +37,44 @@ internal class GamepadWidget : IDataWindowWidget
ImGui.SetClipboardText($"0x{gamepadState.GamepadInputAddress.ToInt64():X}");
#endif
DrawHelper(
this.DrawHelper(
"Buttons Raw",
gamepadState.ButtonsRaw,
gamepadState.Raw);
DrawHelper(
this.DrawHelper(
"Buttons Pressed",
gamepadState.ButtonsPressed,
gamepadState.Pressed);
DrawHelper(
this.DrawHelper(
"Buttons Repeat",
gamepadState.ButtonsRepeat,
gamepadState.Repeat);
DrawHelper(
this.DrawHelper(
"Buttons Released",
gamepadState.ButtonsReleased,
gamepadState.Released);
ImGui.Text($"LeftStickLeft {gamepadState.LeftStickLeft:0.00} " +
$"LeftStickUp {gamepadState.LeftStickUp:0.00} " +
$"LeftStickRight {gamepadState.LeftStickRight:0.00} " +
$"LeftStickDown {gamepadState.LeftStickDown:0.00} ");
ImGui.Text($"RightStickLeft {gamepadState.RightStickLeft:0.00} " +
$"RightStickUp {gamepadState.RightStickUp:0.00} " +
$"RightStickRight {gamepadState.RightStickRight:0.00} " +
$"RightStickDown {gamepadState.RightStickDown:0.00} ");
ImGui.Text($"LeftStick {gamepadState.LeftStick}");
ImGui.Text($"RightStick {gamepadState.RightStick}");
}
private void DrawHelper(string text, uint mask, Func<GamepadButtons, float> resolve)
{
ImGui.Text($"{text} {mask:X4}");
ImGui.Text($"DPadLeft {resolve(GamepadButtons.DpadLeft)} " +
$"DPadUp {resolve(GamepadButtons.DpadUp)} " +
$"DPadRight {resolve(GamepadButtons.DpadRight)} " +
$"DPadDown {resolve(GamepadButtons.DpadDown)} ");
ImGui.Text($"West {resolve(GamepadButtons.West)} " +
$"North {resolve(GamepadButtons.North)} " +
$"East {resolve(GamepadButtons.East)} " +
$"South {resolve(GamepadButtons.South)} ");
ImGui.Text($"L1 {resolve(GamepadButtons.L1)} " +
$"L2 {resolve(GamepadButtons.L2)} " +
$"R1 {resolve(GamepadButtons.R1)} " +
$"R2 {resolve(GamepadButtons.R2)} ");
ImGui.Text($"Select {resolve(GamepadButtons.Select)} " +
$"Start {resolve(GamepadButtons.Start)} " +
$"L3 {resolve(GamepadButtons.L3)} " +
$"R3 {resolve(GamepadButtons.R3)} ");
}
}

View file

@ -0,0 +1,224 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text.RegularExpressions;
using Dalamud.Data;
using Dalamud.Game.Network;
using Dalamud.Interface.Raii;
using Dalamud.Memory;
using ImGuiNET;
namespace Dalamud.Interface.Internal.Windows.Data;
/// <summary>
/// Widget to display the current packets.
/// </summary>
internal class NetworkMonitorWidget : IDataWindowWidget
{
#pragma warning disable SA1313
private readonly record struct NetworkPacketData(ushort OpCode, NetworkMessageDirection Direction, uint SourceActorId, uint TargetActorId)
#pragma warning restore SA1313
{
public readonly IReadOnlyList<byte> Data = Array.Empty<byte>();
public NetworkPacketData(NetworkMonitorWidget widget, ushort opCode, NetworkMessageDirection direction, uint sourceActorId, uint targetActorId, nint dataPtr)
: this(opCode, direction, sourceActorId, targetActorId)
=> this.Data = MemoryHelper.Read<byte>(dataPtr, widget.GetSizeFromOpCode(opCode), false);
}
private readonly ConcurrentQueue<NetworkPacketData> packets = new();
private readonly Dictionary<ushort, (string Name, int Size)> opCodeDict = new();
private bool trackNetwork;
private int trackedPackets;
private Regex? trackedOpCodes;
private string filterString = string.Empty;
private Regex? untrackedOpCodes;
private string negativeFilterString = string.Empty;
/// <summary> Finalizes an instance of the <see cref="NetworkMonitorWidget"/> class. </summary>
~NetworkMonitorWidget()
{
if (this.trackNetwork)
{
this.trackNetwork = false;
var network = Service<GameNetwork>.GetNullable();
if (network != null)
{
network.NetworkMessage -= this.OnNetworkMessage;
}
}
}
/// <inheritdoc/>
public DataKind DataKind { get; init; } = DataKind.Network_Monitor;
/// <inheritdoc/>
public bool Ready { get; set; }
/// <inheritdoc/>
public void Load()
{
this.trackNetwork = false;
this.trackedPackets = 20;
this.trackedOpCodes = null;
this.filterString = string.Empty;
this.packets.Clear();
this.Ready = true;
var dataManager = Service<DataManager>.Get();
foreach (var (name, code) in dataManager.ClientOpCodes.Concat(dataManager.ServerOpCodes))
this.opCodeDict.TryAdd(code, (name, this.GetSizeFromName(name)));
}
/// <inheritdoc/>
public void Draw()
{
var network = Service<GameNetwork>.Get();
if (ImGui.Checkbox("Track Network Packets", ref this.trackNetwork))
{
if (this.trackNetwork)
{
network.NetworkMessage += this.OnNetworkMessage;
}
else
{
network.NetworkMessage -= this.OnNetworkMessage;
}
}
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X / 2);
if (ImGui.DragInt("Stored Number of Packets", ref this.trackedPackets, 0.1f, 1, 512))
{
this.trackedPackets = Math.Clamp(this.trackedPackets, 1, 512);
}
this.DrawFilterInput();
this.DrawNegativeFilterInput();
ImGuiTable.DrawTable(string.Empty, this.packets, this.DrawNetworkPacket, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Direction", "Known Name", "OpCode", "Hex", "Target", "Source", "Data");
}
private void DrawNetworkPacket(NetworkPacketData data)
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(data.Direction.ToString());
ImGui.TableNextColumn();
if (this.opCodeDict.TryGetValue(data.OpCode, out var pair))
{
ImGui.TextUnformatted(pair.Name);
}
else
{
ImGui.Dummy(new Vector2(150 * ImGuiHelpers.GlobalScale, 0));
}
ImGui.TableNextColumn();
ImGui.TextUnformatted(data.OpCode.ToString());
ImGui.TableNextColumn();
ImGui.TextUnformatted($"0x{data.OpCode:X4}");
ImGui.TableNextColumn();
ImGui.TextUnformatted(data.TargetActorId > 0 ? $"0x{data.TargetActorId:X}" : string.Empty);
ImGui.TableNextColumn();
ImGui.TextUnformatted(data.SourceActorId > 0 ? $"0x{data.SourceActorId:X}" : string.Empty);
ImGui.TableNextColumn();
if (data.Data.Count > 0)
{
ImGui.TextUnformatted(string.Join(" ", data.Data.Select(b => b.ToString("X2"))));
}
else
{
ImGui.Dummy(ImGui.GetContentRegionAvail() with { Y = 0 });
}
}
private void DrawFilterInput()
{
var invalidRegEx = this.filterString.Length > 0 && this.trackedOpCodes == null;
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, invalidRegEx);
using var color = ImRaii.PushColor(ImGuiCol.Border, 0xFF0000FF, invalidRegEx);
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
if (!ImGui.InputTextWithHint("##Filter", "Regex Filter OpCodes...", ref this.filterString, 1024))
{
return;
}
if (this.filterString.Length == 0)
{
this.trackedOpCodes = null;
}
else
{
try
{
this.trackedOpCodes = new Regex(this.filterString, RegexOptions.Compiled | RegexOptions.ExplicitCapture);
}
catch
{
this.trackedOpCodes = null;
}
}
}
private void DrawNegativeFilterInput()
{
var invalidRegEx = this.negativeFilterString.Length > 0 && this.untrackedOpCodes == null;
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, invalidRegEx);
using var color = ImRaii.PushColor(ImGuiCol.Border, 0xFF0000FF, invalidRegEx);
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
if (!ImGui.InputTextWithHint("##NegativeFilter", "Regex Filter Against OpCodes...", ref this.negativeFilterString, 1024))
{
return;
}
if (this.negativeFilterString.Length == 0)
{
this.untrackedOpCodes = null;
}
else
{
try
{
this.untrackedOpCodes = new Regex(this.negativeFilterString, RegexOptions.Compiled | RegexOptions.ExplicitCapture);
}
catch
{
this.untrackedOpCodes = null;
}
}
}
private void OnNetworkMessage(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction)
{
if ((this.trackedOpCodes == null || this.trackedOpCodes.IsMatch(this.OpCodeToString(opCode)))
&& (this.untrackedOpCodes == null || !this.untrackedOpCodes.IsMatch(this.OpCodeToString(opCode))))
{
this.packets.Enqueue(new NetworkPacketData(this, opCode, direction, sourceActorId, targetActorId, dataPtr));
while (this.packets.Count > this.trackedPackets)
{
this.packets.TryDequeue(out _);
}
}
}
private int GetSizeFromOpCode(ushort opCode)
=> this.opCodeDict.TryGetValue(opCode, out var pair) ? pair.Size : 0;
/// <remarks> Add known packet-name -> packet struct size associations here to copy the byte data for such packets. </remarks>>
private int GetSizeFromName(string name)
=> name switch
{
_ => 0,
};
/// <remarks> The filter should find opCodes by number (decimal and hex) and name, if existing. </remarks>
private string OpCodeToString(ushort opCode)
=> this.opCodeDict.TryGetValue(opCode, out var pair) ? $"{opCode}\0{opCode:X}\0{pair.Name}" : $"{opCode}\0{opCode:X}";
}

View file

@ -65,20 +65,20 @@ internal class TargetWidget : IDataWindowWidget
Util.PrintGameObject(targetMgr.SoftTarget, "SoftTarget", this.resolveGameData);
if (ImGui.Button("Clear CT"))
targetMgr.ClearTarget();
targetMgr.Target = null;
if (ImGui.Button("Clear FT"))
targetMgr.ClearFocusTarget();
targetMgr.FocusTarget = null;
var localPlayer = clientState.LocalPlayer;
if (localPlayer != null)
{
if (ImGui.Button("Set CT"))
targetMgr.SetTarget(localPlayer);
targetMgr.Target = localPlayer;
if (ImGui.Button("Set FT"))
targetMgr.SetFocusTarget(localPlayer);
targetMgr.FocusTarget = localPlayer;
}
else
{

View file

@ -11,6 +11,7 @@ using Dalamud.Game;
using Dalamud.Networking.Http;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Internal.Types.Manifest;
using Dalamud.Utility;
using ImGuiScene;
using Serilog;
@ -232,7 +233,7 @@ internal class PluginImageCache : IDisposable, IServiceType
/// <param name="isThirdParty">If the plugin was third party sourced.</param>
/// <param name="iconTexture">Cached image textures, or an empty array.</param>
/// <returns>True if an entry exists, may be null if currently downloading.</returns>
public bool TryGetIcon(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, out TextureWrap? iconTexture)
public bool TryGetIcon(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, out TextureWrap? iconTexture)
{
iconTexture = null;
@ -274,7 +275,7 @@ internal class PluginImageCache : IDisposable, IServiceType
/// <param name="isThirdParty">If the plugin was third party sourced.</param>
/// <param name="imageTextures">Cached image textures, or an empty array.</param>
/// <returns>True if the image array exists, may be empty if currently downloading.</returns>
public bool TryGetImages(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, out TextureWrap?[] imageTextures)
public bool TryGetImages(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, out TextureWrap?[] imageTextures)
{
if (!this.pluginImagesMap.TryAdd(manifest.InternalName, null))
{
@ -307,7 +308,7 @@ internal class PluginImageCache : IDisposable, IServiceType
byte[]? bytes,
string name,
string? loc,
PluginManifest manifest,
IPluginManifest manifest,
int maxWidth,
int maxHeight,
bool requireSquare)
@ -491,7 +492,7 @@ internal class PluginImageCache : IDisposable, IServiceType
Log.Debug("Plugin image loader has shutdown");
}
private async Task<TextureWrap?> DownloadPluginIconAsync(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, ulong requestedFrame)
private async Task<TextureWrap?> DownloadPluginIconAsync(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, ulong requestedFrame)
{
if (plugin is { IsDev: true })
{
@ -558,7 +559,7 @@ internal class PluginImageCache : IDisposable, IServiceType
return icon;
}
private async Task DownloadPluginImagesAsync(TextureWrap?[] pluginImages, LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, ulong requestedFrame)
private async Task DownloadPluginImagesAsync(TextureWrap?[] pluginImages, LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, ulong requestedFrame)
{
if (plugin is { IsDev: true })
{
@ -671,18 +672,15 @@ internal class PluginImageCache : IDisposable, IServiceType
}
}
private string? GetPluginIconUrl(PluginManifest manifest, bool isThirdParty, bool isTesting)
private string? GetPluginIconUrl(IPluginManifest manifest, bool isThirdParty, bool isTesting)
{
if (isThirdParty)
return manifest.IconUrl;
if (manifest.IsDip17Plugin)
return MainRepoDip17ImageUrl.Format(manifest.Dip17Channel!, manifest.InternalName, "icon.png");
return MainRepoImageUrl.Format(isTesting ? "testing" : "plugins", manifest.InternalName, "icon.png");
return MainRepoDip17ImageUrl.Format(manifest.Dip17Channel!, manifest.InternalName, "icon.png");
}
private List<string?>? GetPluginImageUrls(PluginManifest manifest, bool isThirdParty, bool isTesting)
private List<string?>? GetPluginImageUrls(IPluginManifest manifest, bool isThirdParty, bool isTesting)
{
if (isThirdParty)
{
@ -698,14 +696,7 @@ internal class PluginImageCache : IDisposable, IServiceType
var output = new List<string>();
for (var i = 1; i <= 5; i++)
{
if (manifest.IsDip17Plugin)
{
output.Add(MainRepoDip17ImageUrl.Format(manifest.Dip17Channel!, manifest.InternalName, $"image{i}.png"));
}
else
{
output.Add(MainRepoImageUrl.Format(isTesting ? "testing" : "plugins", manifest.InternalName, $"image{i}.png"));
}
output.Add(MainRepoDip17ImageUrl.Format(manifest.Dip17Channel!, manifest.InternalName, $"image{i}.png"));
}
return output;

View file

@ -48,15 +48,12 @@ internal class DalamudChangelogManager
foreach (var plugin in this.manager.InstalledPlugins)
{
if (!plugin.Manifest.IsThirdParty)
if (!plugin.IsThirdParty)
{
if (!plugin.Manifest.IsDip17Plugin)
continue;
var pluginChangelogs = await client.GetFromJsonAsync<PluginHistory>(string.Format(
PluginChangelogUrl,
plugin.Manifest.InternalName,
plugin.Manifest.Dip17Channel));
PluginChangelogUrl,
plugin.Manifest.InternalName,
plugin.Manifest.Dip17Channel));
changelogs = changelogs.Concat(pluginChangelogs.Versions
.Where(x => x.Dip17Track == plugin.Manifest.Dip17Channel)

View file

@ -33,7 +33,7 @@ internal class PluginChangelogEntry : IChangelogEntry
{
this.Plugin = plugin;
this.Version = plugin.Manifest.EffectiveVersion.ToString();
this.Version = plugin.EffectiveVersion.ToString();
this.Text = plugin.Manifest.Changelog ?? Loc.Localize("ChangelogNoText", "No changelog for this version.");
this.Author = plugin.Manifest.Author;
this.Date = DateTimeOffset.FromUnixTimeSeconds(this.Plugin.Manifest.LastUpdate).DateTime;

View file

@ -24,6 +24,7 @@ using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Exceptions;
using Dalamud.Plugin.Internal.Profiles;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Internal.Types.Manifest;
using Dalamud.Support;
using Dalamud.Utility;
using ImGuiNET;
@ -88,7 +89,7 @@ internal class PluginInstallerWindow : Window, IDisposable
private string feedbackModalBody = string.Empty;
private string feedbackModalContact = string.Empty;
private bool feedbackModalIncludeException = false;
private PluginManifest? feedbackPlugin = null;
private IPluginManifest? feedbackPlugin = null;
private bool feedbackIsTesting = false;
private int updatePluginCount = 0;
@ -183,6 +184,7 @@ internal class PluginInstallerWindow : Window, IDisposable
NewOrNot,
NotInstalled,
EnabledDisabled,
ProfileOrNot,
}
private bool AnyOperationInProgress => this.installStatus == OperationStatus.InProgress ||
@ -504,6 +506,7 @@ internal class PluginInstallerWindow : Window, IDisposable
(Locs.SortBy_NewOrNot, PluginSortKind.NewOrNot),
(Locs.SortBy_NotInstalled, PluginSortKind.NotInstalled),
(Locs.SortBy_EnabledDisabled, PluginSortKind.EnabledDisabled),
(Locs.SortBy_ProfileOrNot, PluginSortKind.ProfileOrNot),
};
var longestSelectableWidth = sortSelectables.Select(t => ImGui.CalcTextSize(t.Localization).X).Max();
var selectableWidth = longestSelectableWidth + (style.FramePadding.X * 2); // This does not include the label
@ -533,7 +536,8 @@ internal class PluginInstallerWindow : Window, IDisposable
"###XlPluginInstaller_Search",
Locs.Header_SearchPlaceholder,
ref this.searchText,
100);
100,
ImGuiInputTextFlags.AutoSelectAll);
ImGui.SameLine();
ImGui.SetCursorPosY(downShift);
@ -978,7 +982,7 @@ internal class PluginInstallerWindow : Window, IDisposable
changelogs = this.dalamudChangelogManager.Changelogs.OfType<PluginChangelogEntry>();
}
var sortedChangelogs = changelogs?.Where(x => this.searchText.IsNullOrWhitespace() || x.Title.ToLowerInvariant().Contains(this.searchText.ToLowerInvariant()))
var sortedChangelogs = changelogs?.Where(x => this.searchText.IsNullOrWhitespace() || new FuzzyMatcher(this.searchText.ToLowerInvariant(), MatchMode.FuzzyParts).Matches(x.Title.ToLowerInvariant()) > 0)
.OrderByDescending(x => x.Date).ToList();
if (sortedChangelogs == null || !sortedChangelogs.Any())
@ -1606,7 +1610,7 @@ internal class PluginInstallerWindow : Window, IDisposable
return ready;
}
private bool DrawPluginCollapsingHeader(string label, LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, bool trouble, bool updateAvailable, bool isNew, bool installableOutdated, bool isOrphan, Action drawContextMenuAction, int index)
private bool DrawPluginCollapsingHeader(string label, LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, bool trouble, bool updateAvailable, bool isNew, bool installableOutdated, bool isOrphan, Action drawContextMenuAction, int index)
{
ImGui.Separator();
@ -1741,13 +1745,13 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.TextWrapped(Locs.PluginBody_Orphaned);
ImGui.PopStyleColor();
}
else if (plugin is { IsDecommissioned: true } && !plugin.Manifest.IsThirdParty)
else if (plugin is { IsDecommissioned: true, IsThirdParty: false })
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGui.TextWrapped(Locs.PluginBody_NoServiceOfficial);
ImGui.PopStyleColor();
}
else if (plugin is { IsDecommissioned: true } && plugin.Manifest.IsThirdParty)
else if (plugin is { IsDecommissioned: true, IsThirdParty: true })
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGui.TextWrapped(Locs.PluginBody_NoServiceThird);
@ -1808,7 +1812,7 @@ internal class PluginInstallerWindow : Window, IDisposable
if (log is PluginChangelogEntry pluginLog)
{
icon = this.imageCache.DefaultIcon;
var hasIcon = this.imageCache.TryGetIcon(pluginLog.Plugin, pluginLog.Plugin.Manifest, pluginLog.Plugin.Manifest.IsThirdParty, out var cachedIconTex);
var hasIcon = this.imageCache.TryGetIcon(pluginLog.Plugin, pluginLog.Plugin.Manifest, pluginLog.Plugin.IsThirdParty, out var cachedIconTex);
if (hasIcon && cachedIconTex != null)
{
icon = cachedIconTex;
@ -2031,12 +2035,14 @@ internal class PluginInstallerWindow : Window, IDisposable
}
// Testing
if (plugin.Manifest.Testing)
if (plugin.IsTesting)
{
label += Locs.PluginTitleMod_TestingVersion;
}
if (plugin.Manifest.IsAvailableForTesting && configuration.DoPluginTest && testingOptIn == null)
var hasTestingAvailable = this.pluginListAvailable.Any(x => x.InternalName == plugin.InternalName &&
x.IsAvailableForTesting);
if (hasTestingAvailable && configuration.DoPluginTest && testingOptIn == null)
{
label += Locs.PluginTitleMod_TestingAvailable;
}
@ -2132,7 +2138,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.PushID($"installed{index}{plugin.Manifest.InternalName}");
var hasChangelog = !plugin.Manifest.Changelog.IsNullOrEmpty();
if (this.DrawPluginCollapsingHeader(label, plugin, plugin.Manifest, plugin.Manifest.IsThirdParty, trouble, availablePluginUpdate != default, false, false, plugin.IsOrphaned, () => this.DrawInstalledPluginContextMenu(plugin, testingOptIn), index))
if (this.DrawPluginCollapsingHeader(label, plugin, plugin.Manifest, plugin.IsThirdParty, trouble, availablePluginUpdate != default, false, false, plugin.IsOrphaned, () => this.DrawInstalledPluginContextMenu(plugin, testingOptIn), index))
{
if (!this.WasPluginSeen(plugin.Manifest.InternalName))
configuration.SeenPluginInternalName.Add(plugin.Manifest.InternalName);
@ -2154,12 +2160,15 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.SameLine();
ImGui.TextColored(ImGuiColors.DalamudGrey3, downloadText);
var isThirdParty = manifest.IsThirdParty;
var acceptsFeedback =
this.pluginListAvailable.Any(x => x.InternalName == plugin.InternalName && x.AcceptsFeedback);
var isThirdParty = plugin.IsThirdParty;
var canFeedback = !isThirdParty &&
!plugin.IsDev &&
!plugin.IsOrphaned &&
plugin.Manifest.DalamudApiLevel == PluginManager.DalamudApiLevel &&
plugin.Manifest.AcceptsFeedback &&
acceptsFeedback &&
availablePluginUpdate == default;
// Installed from
@ -2180,6 +2189,14 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGuiHelpers.SafeTextWrapped(manifest.Description);
}
// Working Plugin ID
if (this.hasDevPlugins)
{
ImGuiHelpers.ScaledDummy(3);
ImGui.TextColored(ImGuiColors.DalamudGrey, $"WorkingPluginId: {manifest.WorkingPluginId}");
ImGuiHelpers.ScaledDummy(3);
}
// Available commands (if loaded)
if (plugin.IsLoaded)
{
@ -2215,7 +2232,7 @@ internal class PluginInstallerWindow : Window, IDisposable
this.DrawUpdateSinglePluginButton(availablePluginUpdate);
ImGui.SameLine();
ImGui.TextColored(ImGuiColors.DalamudGrey3, $" v{plugin.Manifest.EffectiveVersion}");
ImGui.TextColored(ImGuiColors.DalamudGrey3, $" v{plugin.EffectiveVersion}");
ImGuiHelpers.ScaledDummy(5);
@ -2226,7 +2243,7 @@ internal class PluginInstallerWindow : Window, IDisposable
if (hasChangelog)
{
if (ImGui.TreeNode(Locs.PluginBody_CurrentChangeLog(plugin.Manifest.EffectiveVersion)))
if (ImGui.TreeNode(Locs.PluginBody_CurrentChangeLog(plugin.EffectiveVersion)))
{
this.DrawInstalledPluginChangelog(plugin.Manifest);
ImGui.TreePop();
@ -2252,7 +2269,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.PopID();
}
private void DrawInstalledPluginChangelog(PluginManifest manifest)
private void DrawInstalledPluginChangelog(IPluginManifest manifest)
{
ImGuiHelpers.ScaledDummy(5);
@ -2265,7 +2282,7 @@ internal class PluginInstallerWindow : Window, IDisposable
{
ImGui.Text("Changelog:");
ImGuiHelpers.ScaledDummy(2);
ImGuiHelpers.SafeTextWrapped(manifest.Changelog);
ImGuiHelpers.SafeTextWrapped(manifest.Changelog!);
}
ImGui.EndChild();
@ -2336,7 +2353,7 @@ internal class PluginInstallerWindow : Window, IDisposable
var profileManager = Service<ProfileManager>.Get();
var config = Service<DalamudConfiguration>.Get();
var applicableForProfiles = plugin.Manifest.SupportsProfiles;
var applicableForProfiles = plugin.Manifest.SupportsProfiles && !plugin.IsDev;
var isDefaultPlugin = profileManager.IsInDefaultProfile(plugin.Manifest.InternalName);
// Disable everything if the updater is running or another plugin is operating
@ -2363,7 +2380,7 @@ internal class PluginInstallerWindow : Window, IDisposable
var isLoadedAndUnloadable = plugin.State == PluginState.Loaded ||
plugin.State == PluginState.DependencyResolutionFailed;
//StyleModelV1.DalamudStandard.Push();
// StyleModelV1.DalamudStandard.Push();
var profileChooserPopupName = $"###pluginProfileChooser{plugin.Manifest.InternalName}";
if (ImGui.BeginPopup(profileChooserPopupName))
@ -2526,7 +2543,7 @@ internal class PluginInstallerWindow : Window, IDisposable
}
}
//StyleModelV1.DalamudStandard.Pop();
// StyleModelV1.DalamudStandard.Pop();
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(15, 0);
@ -2621,7 +2638,7 @@ internal class PluginInstallerWindow : Window, IDisposable
}
}
private void DrawSendFeedbackButton(PluginManifest manifest, bool isTesting)
private void DrawSendFeedbackButton(IPluginManifest manifest, bool isTesting)
{
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.Comment))
@ -2796,7 +2813,7 @@ internal class PluginInstallerWindow : Window, IDisposable
}
}
private bool DrawPluginImages(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, int index)
private bool DrawPluginImages(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, int index)
{
var hasImages = this.imageCache.TryGetImages(plugin, manifest, isThirdParty, out var imageTextures);
if (!hasImages || imageTextures.All(x => x == null))
@ -2871,10 +2888,10 @@ internal class PluginInstallerWindow : Window, IDisposable
return true;
}
private bool IsManifestFiltered(PluginManifest manifest)
private bool IsManifestFiltered(IPluginManifest manifest)
{
var searchString = this.searchText.ToLowerInvariant();
var hasSearchString = !string.IsNullOrWhiteSpace(searchString);
var matcher = new FuzzyMatcher(this.searchText.ToLowerInvariant(), MatchMode.FuzzyParts);
var hasSearchString = !string.IsNullOrWhiteSpace(this.searchText);
var oldApi = manifest.DalamudApiLevel < PluginManager.DalamudApiLevel;
var installed = this.IsManifestInstalled(manifest).IsInstalled;
@ -2882,14 +2899,14 @@ internal class PluginInstallerWindow : Window, IDisposable
return true;
return hasSearchString && !(
(!manifest.Name.IsNullOrEmpty() && manifest.Name.ToLowerInvariant().Contains(searchString)) ||
(!manifest.InternalName.IsNullOrEmpty() && manifest.InternalName.ToLowerInvariant().Contains(searchString)) ||
(!manifest.Author.IsNullOrEmpty() && manifest.Author.Equals(this.searchText, StringComparison.InvariantCultureIgnoreCase)) ||
(!manifest.Punchline.IsNullOrEmpty() && manifest.Punchline.ToLowerInvariant().Contains(searchString)) ||
(manifest.Tags != null && manifest.Tags.Any(tag => tag.ToLowerInvariant().Contains(searchString))));
(!manifest.Name.IsNullOrEmpty() && matcher.Matches(manifest.Name.ToLowerInvariant()) > 0) ||
(!manifest.InternalName.IsNullOrEmpty() && matcher.Matches(manifest.InternalName.ToLowerInvariant()) > 0) ||
(!manifest.Author.IsNullOrEmpty() && matcher.Matches(manifest.Author.ToLowerInvariant()) > 0) ||
// (!manifest.Punchline.IsNullOrEmpty() && matcher.Matches(manifest.Punchline.ToLowerInvariant()) > 0) || // Removed because fuzzy match gets a little too excited with lots of random words
(manifest.Tags != null && matcher.MatchesAny(manifest.Tags.Select(term => term.ToLowerInvariant()).ToArray()) > 0));
}
private (bool IsInstalled, LocalPlugin Plugin) IsManifestInstalled(PluginManifest? manifest)
private (bool IsInstalled, LocalPlugin Plugin) IsManifestInstalled(IPluginManifest? manifest)
{
if (manifest == null) return (false, default);
@ -2970,6 +2987,12 @@ internal class PluginInstallerWindow : Window, IDisposable
});
this.pluginListInstalled.Sort((p1, p2) => (p2.State == PluginState.Loaded).CompareTo(p1.State == PluginState.Loaded));
break;
case PluginSortKind.ProfileOrNot:
this.pluginListAvailable.Sort((p1, p2) => p1.Name.CompareTo(p2.Name));
var profman = Service<ProfileManager>.Get();
this.pluginListInstalled.Sort((p1, p2) => profman.IsInDefaultProfile(p1.InternalName).CompareTo(profman.IsInDefaultProfile(p2.InternalName)));
break;
default:
throw new InvalidEnumArgumentException("Unknown plugin sort type.");
}
@ -3048,6 +3071,8 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string SortBy_EnabledDisabled => Loc.Localize("InstallerEnabledDisabled", "Enabled/Disabled");
public static string SortBy_ProfileOrNot => Loc.Localize("InstallerProfileOrNot", "In a collection");
public static string SortBy_Label => Loc.Localize("InstallerSortBy", "Sort By");
#endregion
@ -3124,9 +3149,9 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string PluginContext_HidePlugin => Loc.Localize("InstallerHidePlugin", "Hide from installer");
public static string PluginContext_DeletePluginConfig => Loc.Localize("InstallerDeletePluginConfig", "Reset plugin configuration");
public static string PluginContext_DeletePluginConfig => Loc.Localize("InstallerDeletePluginConfig", "Reset plugin data");
public static string PluginContext_DeletePluginConfigReload => Loc.Localize("InstallerDeletePluginConfigReload", "Reset plugin configuration and reload");
public static string PluginContext_DeletePluginConfigReload => Loc.Localize("InstallerDeletePluginConfigReload", "Reset plugin data and reload");
#endregion

View file

@ -4,6 +4,7 @@ using System.Numerics;
using System.Threading.Tasks;
using CheapLoc;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Internal.Notifications;
@ -48,10 +49,14 @@ internal class ProfileManagerWidget
/// </summary>
public void Draw()
{
var tutorialTitle = Locs.TutorialTitle + "###collectionsTutorWindow";
var tutorialId = ImGui.GetID(tutorialTitle);
this.DrawTutorial(tutorialTitle);
switch (this.mode)
{
case Mode.Overview:
this.DrawOverview();
this.DrawOverview(tutorialId);
break;
case Mode.EditSingleProfile:
@ -70,7 +75,62 @@ internal class ProfileManagerWidget
this.pickerSearch = string.Empty;
}
private void DrawOverview()
private void DrawTutorial(string modalTitle)
{
var open = true;
ImGui.SetNextWindowSize(new Vector2(450, 350), ImGuiCond.Appearing);
using (var popup = ImRaii.PopupModal(modalTitle, ref open))
{
if (popup)
{
using var scrolling = ImRaii.Child("###scrolling", new Vector2(-1, -1));
if (scrolling)
{
ImGuiHelpers.SafeTextWrapped(Locs.TutorialParagraphOne);
ImGuiHelpers.ScaledDummy(5);
ImGuiHelpers.SafeTextWrapped(Locs.TutorialParagraphTwo);
ImGuiHelpers.ScaledDummy(5);
ImGuiHelpers.SafeTextWrapped(Locs.TutorialParagraphThree);
ImGuiHelpers.ScaledDummy(5);
ImGuiHelpers.SafeTextWrapped(Locs.TutorialParagraphFour);
ImGuiHelpers.ScaledDummy(5);
ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommands);
ImGui.Bullet();
ImGui.SameLine();
ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommandsEnable);
ImGui.Bullet();
ImGui.SameLine();
ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommandsDisable);
ImGui.Bullet();
ImGui.SameLine();
ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommandsToggle);
ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommandsEnd);
ImGuiHelpers.ScaledDummy(5);
var buttonWidth = 120f;
ImGui.SetCursorPosX((ImGui.GetWindowWidth() - buttonWidth) / 2);
if (ImGui.Button("OK", new Vector2(buttonWidth, 40)))
{
ImGui.CloseCurrentPopup();
}
}
}
}
var config = Service<DalamudConfiguration>.Get();
if (!config.ProfilesHasSeenTutorial)
{
ImGui.OpenPopup(modalTitle);
config.ProfilesHasSeenTutorial = true;
config.QueueSave();
}
}
private void DrawOverview(uint tutorialId)
{
var didAny = false;
var profman = Service<ProfileManager>.Get();
@ -101,6 +161,16 @@ internal class ProfileManagerWidget
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.ImportProfileHint);
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(5);
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.Question))
ImGui.OpenPopup(tutorialId);
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.TutorialHint);
ImGui.Separator();
ImGuiHelpers.ScaledDummy(5);
@ -221,7 +291,7 @@ internal class ProfileManagerWidget
if (ImGui.BeginListBox("###pluginPicker", new Vector2(width, width - 80)))
{
// TODO: Plugin searching should be abstracted... installer and this should use the same search
foreach (var plugin in pm.InstalledPlugins.Where(x => x.Manifest.SupportsProfiles &&
foreach (var plugin in pm.InstalledPlugins.Where(x => x.Manifest.SupportsProfiles && !x.IsDev &&
(this.pickerSearch.IsNullOrWhitespace() || x.Manifest.Name.ToLowerInvariant().Contains(this.pickerSearch.ToLowerInvariant()))))
{
using var disabled2 =
@ -340,7 +410,7 @@ internal class ProfileManagerWidget
if (pmPlugin != null)
{
pic.TryGetIcon(pmPlugin, pmPlugin.Manifest, pmPlugin.Manifest.IsThirdParty, out var icon);
pic.TryGetIcon(pmPlugin, pmPlugin.Manifest, pmPlugin.IsThirdParty, out var icon);
icon ??= pic.DefaultIcon;
ImGui.Image(icon.ImGuiHandle, new Vector2(pluginLineHeight));
@ -488,6 +558,9 @@ internal class ProfileManagerWidget
public static string ImportProfileHint =>
Loc.Localize("ProfileManagerImportProfile", "Import a shared collection from your clipboard");
public static string TutorialHint =>
Loc.Localize("ProfileManagerTutorialHint", "Learn more about collections");
public static string AddProfile => Loc.Localize("ProfileManagerAddProfile", "Add a new collection");
public static string NotificationImportSuccess =>
@ -502,6 +575,36 @@ internal class ProfileManagerWidget
public static string ErrorCouldNotChangeState =>
Loc.Localize("ProfileManagerCouldNotChangeState", "Could not change plugin state.");
public static string TutorialTitle =>
Loc.Localize("ProfileManagerTutorial", "About Collections");
public static string TutorialParagraphOne =>
Loc.Localize("ProfileManagerTutorialParagraphOne", "Collections are shareable lists of plugins that can be enabled or disabled in the plugin installer or via chat commands.\nWhen a plugin is part of a collection, it will be enabled if the collection is enabled. If a plugin is part of multiple collections, it will be enabled if one or more collections it is a part of are enabled.");
public static string TutorialParagraphTwo =>
Loc.Localize("ProfileManagerTutorialParagraphTwo", "You can add plugins to collections by clicking the plus button when editing a collection on this screen, or by using the button with the toolbox icon on the \"Installed Plugins\" screen.");
public static string TutorialParagraphThree =>
Loc.Localize("ProfileManagerTutorialParagraphThree", "If a collection's \"Start on boot\" checkbox is ticked, the collection and the plugins within will be enabled every time the game starts up, even if it has been manually disabled in a prior session.");
public static string TutorialParagraphFour =>
Loc.Localize("ProfileManagerTutorialParagraphFour", "Individual plugins inside a collection also have a checkbox next to them. This indicates if a plugin is active within that collection - if the checkbox is not ticked, the plugin will not be enabled if that collection is active. Mind that it will still be enabled if the plugin is an active part of any other collection.");
public static string TutorialCommands =>
Loc.Localize("ProfileManagerTutorialCommands", "You can use the following commands in chat or in macros to manage active collections:");
public static string TutorialCommandsEnable =>
Loc.Localize("ProfileManagerTutorialCommandsEnable", "/xlenableprofile \"Collection Name\" - Enable a collection");
public static string TutorialCommandsDisable =>
Loc.Localize("ProfileManagerTutorialCommandsDisable", "/xldisableprofile \"Collection Name\" - Disable a collection");
public static string TutorialCommandsToggle =>
Loc.Localize("ProfileManagerTutorialCommandsToggle", "/xltoggleprofile \"Collection Name\" - Toggle a collection's state");
public static string TutorialCommandsEnd =>
Loc.Localize("ProfileManagerTutorialCommandsEnd", "If you run multiple of these commands, they will be executed in order.");
public static string NotInstalled(string name) =>
Loc.Localize("ProfileManagerNotInstalled", "{0} (Not Installed)").Format(name);
}

View file

@ -23,8 +23,8 @@ internal class TargetAgingStep : IAgingStep
switch (this.step)
{
case 0:
targetManager.ClearTarget();
targetManager.ClearFocusTarget();
targetManager.Target = null;
targetManager.FocusTarget = null;
this.step++;

View file

@ -101,6 +101,12 @@ public class SettingsTabLook : SettingsTab
Loc.Localize("DalamudSettingToggleDockingHint", "This will allow you to fuse and tab plugin windows."),
c => c.IsDocking,
(v, c) => c.IsDocking = v),
new SettingsEntry<bool>(
Loc.Localize("DalamudSettingEnablePluginUISoundEffects", "Enable sound effects for plugin windows"),
Loc.Localize("DalamudSettingEnablePluginUISoundEffectsHint", "This will allow you to enable or disable sound effects generated by plugin user interfaces.\nThis is affected by your in-game `System Sounds` volume settings."),
c => c.EnablePluginUISoundEffects,
(v, c) => c.EnablePluginUISoundEffects = v),
new SettingsEntry<bool>(
Loc.Localize("DalamudSettingToggleGamepadNavigation", "Control plugins via gamepad"),

View file

@ -45,7 +45,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus)
{
this.IsOpen = true;
this.DisableWindowSounds = true;
this.ForceMainWindow = true;
this.Position = new Vector2(0, 200);

View file

@ -2,6 +2,7 @@ using System.Numerics;
using Dalamud.Configuration.Internal;
using Dalamud.Game.ClientState.Keys;
using FFXIVClientStructs.FFXIV.Client.UI;
using ImGuiNET;
namespace Dalamud.Interface.Windowing;
@ -55,6 +56,21 @@ public abstract class Window
/// </summary>
public bool RespectCloseHotkey { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether this window should not generate sound effects when opening and closing.
/// </summary>
public bool DisableWindowSounds { get; set; } = false;
/// <summary>
/// Gets or sets a value representing the sound effect id to be played when the window is opened.
/// </summary>
public uint OnOpenSfxId { get; set; } = 23u;
/// <summary>
/// Gets or sets a value representing the sound effect id to be played when the window is closed.
/// </summary>
public uint OnCloseSfxId { get; set; } = 24u;
/// <summary>
/// Gets or sets the position of this window.
/// </summary>
@ -207,10 +223,12 @@ public abstract class Window
/// <summary>
/// Draw the window via ImGui.
/// </summary>
internal void DrawInternal()
internal void DrawInternal(DalamudConfiguration? configuration)
{
this.PreOpenCheck();
var doSoundEffects = configuration?.EnablePluginUISoundEffects ?? false;
if (!this.IsOpen)
{
if (this.internalIsOpen != this.internalLastIsOpen)
@ -219,6 +237,8 @@ public abstract class Window
this.OnClose();
this.IsFocused = false;
if (doSoundEffects && !this.DisableWindowSounds) UIModule.PlaySound(this.OnCloseSfxId, 0, 0, 0);
}
return;
@ -243,6 +263,8 @@ public abstract class Window
{
this.internalLastIsOpen = this.internalIsOpen;
this.OnOpen();
if (doSoundEffects && !this.DisableWindowSounds) UIModule.PlaySound(this.OnOpenSfxId, 0, 0, 0);
}
var wasFocused = this.IsFocused;
@ -272,16 +294,19 @@ public abstract class Window
this.IsFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows);
var escapeDown = Service<KeyState>.Get()[VirtualKey.ESCAPE];
var isAllowed = Service<DalamudConfiguration>.Get().IsFocusManagementEnabled;
if (escapeDown && this.IsFocused && isAllowed && !wasEscPressedLastFrame && this.RespectCloseHotkey)
var isAllowed = configuration?.IsFocusManagementEnabled ?? false;
if (isAllowed)
{
this.IsOpen = false;
wasEscPressedLastFrame = true;
}
else if (!escapeDown && wasEscPressedLastFrame)
{
wasEscPressedLastFrame = false;
var escapeDown = Service<KeyState>.Get()[VirtualKey.ESCAPE];
if (escapeDown && this.IsFocused && !wasEscPressedLastFrame && this.RespectCloseHotkey)
{
this.IsOpen = false;
wasEscPressedLastFrame = true;
}
else if (!escapeDown && wasEscPressedLastFrame)
{
wasEscPressedLastFrame = false;
}
}
ImGui.End();

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Internal.ManagedAsserts;
using ImGuiNET;
using Serilog;
@ -111,6 +112,8 @@ public class WindowSystem
if (hasNamespace)
ImGui.PushID(this.Namespace);
var config = Service<DalamudConfiguration>.GetNullable();
// Shallow clone the list of windows so that we can edit it without modifying it while the loop is iterating
foreach (var window in this.windows.ToArray())
{
@ -119,7 +122,7 @@ public class WindowSystem
#endif
var snapshot = ImGuiManagedAsserts.GetSnapshot();
window.DrawInternal();
window.DrawInternal(config);
var source = ($"{this.Namespace}::" ?? string.Empty) + window.WindowName;
ImGuiManagedAsserts.ReportProblems(source, snapshot);