Skip to content

Commit

Permalink
Add IContextMenu service (goatcorp#1682)
Browse files Browse the repository at this point in the history
  • Loading branch information
WorkingRobot authored Feb 29, 2024
1 parent 3d59fa3 commit 5f62c70
Show file tree
Hide file tree
Showing 14 changed files with 1,382 additions and 141 deletions.
560 changes: 560 additions & 0 deletions Dalamud/Game/Gui/ContextMenu/ContextMenu.cs

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions Dalamud/Game/Gui/ContextMenu/ContextMenuType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Dalamud.Game.Gui.ContextMenu;

/// <summary>
/// The type of context menu.
/// Each one has a different associated <see cref="MenuTarget"/>.
/// </summary>
public enum ContextMenuType
{
/// <summary>
/// The default context menu.
/// </summary>
Default,

/// <summary>
/// The inventory context menu. Used when right-clicked on an item.
/// </summary>
Inventory,
}
77 changes: 77 additions & 0 deletions Dalamud/Game/Gui/ContextMenu/MenuArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.Collections.Generic;

using Dalamud.Memory;
using Dalamud.Plugin.Services;

using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;

namespace Dalamud.Game.Gui.ContextMenu;

/// <summary>
/// Base class for <see cref="IContextMenu"/> menu args.
/// </summary>
public abstract unsafe class MenuArgs
{
private IReadOnlySet<nint>? eventInterfaces;

/// <summary>
/// Initializes a new instance of the <see cref="MenuArgs"/> class.
/// </summary>
/// <param name="addon">Addon associated with the context menu.</param>
/// <param name="agent">Agent associated with the context menu.</param>
/// <param name="type">The type of context menu.</param>
/// <param name="eventInterfaces">List of AtkEventInterfaces associated with the context menu.</param>
protected internal MenuArgs(AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet<nint>? eventInterfaces)
{
this.AddonName = addon != null ? MemoryHelper.ReadString((nint)addon->Name, 32) : null;
this.AddonPtr = (nint)addon;
this.AgentPtr = (nint)agent;
this.MenuType = type;
this.eventInterfaces = eventInterfaces;
this.Target = type switch
{
ContextMenuType.Default => new MenuTargetDefault((AgentContext*)agent),
ContextMenuType.Inventory => new MenuTargetInventory((AgentInventoryContext*)agent),
_ => throw new ArgumentException("Invalid context menu type", nameof(type)),
};
}

/// <summary>
/// Gets the name of the addon that opened the context menu.
/// </summary>
public string? AddonName { get; }

/// <summary>
/// Gets the memory pointer of the addon that opened the context menu.
/// </summary>
public nint AddonPtr { get; }

/// <summary>
/// Gets the memory pointer of the agent that opened the context menu.
/// </summary>
public nint AgentPtr { get; }

/// <summary>
/// Gets the type of the context menu.
/// </summary>
public ContextMenuType MenuType { get; }

/// <summary>
/// Gets the target info of the context menu. The actual type depends on <see cref="MenuType"/>.
/// <see cref="ContextMenuType.Default"/> signifies a <see cref="MenuTargetDefault"/>.
/// <see cref="ContextMenuType.Inventory"/> signifies a <see cref="MenuTargetInventory"/>.
/// </summary>
public MenuTarget Target { get; }

/// <summary>
/// Gets a list of AtkEventInterface pointers associated with the context menu.
/// Only available with <see cref="ContextMenuType.Default"/>.
/// Almost always an agent pointer. You can use this to find out what type of context menu it is.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when the context menu is not a <see cref="ContextMenuType.Default"/>.</exception>
public IReadOnlySet<nint> EventInterfaces =>
this.MenuType != ContextMenuType.Default ?
this.eventInterfaces :
throw new InvalidOperationException("Not a default context menu");
}
91 changes: 91 additions & 0 deletions Dalamud/Game/Gui/ContextMenu/MenuItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;

using Lumina.Excel.GeneratedSheets;

namespace Dalamud.Game.Gui.ContextMenu;

/// <summary>
/// A menu item that can be added to a context menu.
/// </summary>
public sealed record MenuItem
{
/// <summary>
/// Gets or sets the display name of the menu item.
/// </summary>
public SeString Name { get; set; } = SeString.Empty;

/// <summary>
/// Gets or sets the prefix attached to the beginning of <see cref="Name"/>.
/// </summary>
public SeIconChar? Prefix { get; set; }

/// <summary>
/// Sets the character to prefix the <see cref="Name"/> with. Will be converted into a fancy boxed letter icon. Must be an uppercase letter.
/// </summary>
/// <exception cref="ArgumentException"><paramref name="value"/> must be an uppercase letter.</exception>
public char? PrefixChar
{
set
{
if (value is { } prefix)
{
if (!char.IsAsciiLetterUpper(prefix))
throw new ArgumentException("Prefix must be an uppercase letter", nameof(value));

this.Prefix = SeIconChar.BoxedLetterA + prefix - 'A';
}
else
{
this.Prefix = null;
}
}
}

/// <summary>
/// Gets or sets the color of the <see cref="Prefix"/>. Specifies a <see cref="UIColor"/> row id.
/// </summary>
public ushort PrefixColor { get; set; }

/// <summary>
/// Gets or sets the callback to be invoked when the menu item is clicked.
/// </summary>
public Action<MenuItemClickedArgs>? OnClicked { get; set; }

/// <summary>
/// Gets or sets the priority (or order) with which the menu item should be displayed in descending order.
/// Priorities below 0 will be displayed above the native menu items.
/// Other priorities will be displayed below the native menu items.
/// </summary>
public int Priority { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the menu item is enabled.
/// Disabled items will be faded and cannot be clicked on.
/// </summary>
public bool IsEnabled { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether the menu item is a submenu.
/// This value is purely visual. Submenu items will have an arrow to its right.
/// </summary>
public bool IsSubmenu { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the menu item is a return item.
/// This value is purely visual. Return items will have a back arrow to its left.
/// If both <see cref="IsSubmenu"/> and <see cref="IsReturn"/> are true, the return arrow will take precedence.
/// </summary>
public bool IsReturn { get; set; }

/// <summary>
/// Gets the name with the given prefix.
/// </summary>
internal SeString PrefixedName =>
this.Prefix is { } prefix
? new SeStringBuilder()
.AddUiForeground($"{prefix.ToIconString()} ", this.PrefixColor)
.Append(this.Name)
.Build()
: this.Name;
}
44 changes: 44 additions & 0 deletions Dalamud/Game/Gui/ContextMenu/MenuItemClickedArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System.Collections.Generic;

using Dalamud.Game.Text.SeStringHandling;

using FFXIVClientStructs.FFXIV.Component.GUI;

namespace Dalamud.Game.Gui.ContextMenu;

/// <summary>
/// Callback args used when a menu item is clicked.
/// </summary>
public sealed unsafe class MenuItemClickedArgs : MenuArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="MenuItemClickedArgs"/> class.
/// </summary>
/// <param name="openSubmenu">Callback for opening a submenu.</param>
/// <param name="addon">Addon associated with the context menu.</param>
/// <param name="agent">Agent associated with the context menu.</param>
/// <param name="type">The type of context menu.</param>
/// <param name="eventInterfaces">List of AtkEventInterfaces associated with the context menu.</param>
internal MenuItemClickedArgs(Action<SeString?, IReadOnlyList<MenuItem>> openSubmenu, AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet<nint> eventInterfaces)
: base(addon, agent, type, eventInterfaces)
{
this.OnOpenSubmenu = openSubmenu;
}

private Action<SeString?, IReadOnlyList<MenuItem>> OnOpenSubmenu { get; }

/// <summary>
/// Opens a submenu with the given name and items.
/// </summary>
/// <param name="name">The name of the submenu, displayed at the top.</param>
/// <param name="items">The items to display in the submenu.</param>
public void OpenSubmenu(SeString name, IReadOnlyList<MenuItem> items) =>
this.OnOpenSubmenu(name, items);

/// <summary>
/// Opens a submenu with the given items.
/// </summary>
/// <param name="items">The items to display in the submenu.</param>
public void OpenSubmenu(IReadOnlyList<MenuItem> items) =>
this.OnOpenSubmenu(null, items);
}
34 changes: 34 additions & 0 deletions Dalamud/Game/Gui/ContextMenu/MenuOpenedArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Collections.Generic;

using FFXIVClientStructs.FFXIV.Component.GUI;

namespace Dalamud.Game.Gui.ContextMenu;

/// <summary>
/// Callback args used when a menu item is opened.
/// </summary>
public sealed unsafe class MenuOpenedArgs : MenuArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="MenuOpenedArgs"/> class.
/// </summary>
/// <param name="addMenuItem">Callback for adding a custom menu item.</param>
/// <param name="addon">Addon associated with the context menu.</param>
/// <param name="agent">Agent associated with the context menu.</param>
/// <param name="type">The type of context menu.</param>
/// <param name="eventInterfaces">List of AtkEventInterfaces associated with the context menu.</param>
internal MenuOpenedArgs(Action<MenuItem> addMenuItem, AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet<nint> eventInterfaces)
: base(addon, agent, type, eventInterfaces)
{
this.OnAddMenuItem = addMenuItem;
}

private Action<MenuItem> OnAddMenuItem { get; }

/// <summary>
/// Adds a custom menu item to the context menu.
/// </summary>
/// <param name="item">The menu item to add.</param>
public void AddMenuItem(MenuItem item) =>
this.OnAddMenuItem(item);
}
9 changes: 9 additions & 0 deletions Dalamud/Game/Gui/ContextMenu/MenuTarget.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Dalamud.Game.Gui.ContextMenu;

/// <summary>
/// Base class for <see cref="MenuArgs"/> contexts.
/// Discriminated based on <see cref="ContextMenuType"/>.
/// </summary>
public abstract class MenuTarget
{
}
67 changes: 67 additions & 0 deletions Dalamud/Game/Gui/ContextMenu/MenuTargetDefault.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Game.Network.Structures.InfoProxy;

using FFXIVClientStructs.FFXIV.Client.UI.Agent;

using Lumina.Excel.GeneratedSheets;

namespace Dalamud.Game.Gui.ContextMenu;

/// <summary>
/// Target information on a default context menu.
/// </summary>
public sealed unsafe class MenuTargetDefault : MenuTarget
{
/// <summary>
/// Initializes a new instance of the <see cref="MenuTargetDefault"/> class.
/// </summary>
/// <param name="context">The agent associated with the context menu.</param>
internal MenuTargetDefault(AgentContext* context)
{
this.Context = context;
}

/// <summary>
/// Gets the name of the target.
/// </summary>
public string TargetName => this.Context->TargetName.ToString();

/// <summary>
/// Gets the object id of the target.
/// </summary>
public ulong TargetObjectId => this.Context->TargetObjectId;

/// <summary>
/// Gets the target object.
/// </summary>
public GameObject? TargetObject => Service<ObjectTable>.Get().SearchById(this.TargetObjectId);

/// <summary>
/// Gets the content id of the target.
/// </summary>
public ulong TargetContentId => this.Context->TargetContentId;

/// <summary>
/// Gets the home world id of the target.
/// </summary>
public ExcelResolver<World> TargetHomeWorld => new((uint)this.Context->TargetHomeWorldId);

/// <summary>
/// Gets the currently targeted character. Only shows up for specific targets, like friends, party finder listings, or party members.
/// Just because this is <see langword="null"/> doesn't mean the target isn't a character.
/// </summary>
public CharacterData? TargetCharacter
{
get
{
var target = this.Context->CurrentContextMenuTarget;
if (target != null)
return new(target);
return null;
}
}

private AgentContext* Context { get; }
}
36 changes: 36 additions & 0 deletions Dalamud/Game/Gui/ContextMenu/MenuTargetInventory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Dalamud.Game.Inventory;

using FFXIVClientStructs.FFXIV.Client.UI.Agent;

namespace Dalamud.Game.Gui.ContextMenu;

/// <summary>
/// Target information on an inventory context menu.
/// </summary>
public sealed unsafe class MenuTargetInventory : MenuTarget
{
/// <summary>
/// Initializes a new instance of the <see cref="MenuTargetInventory"/> class.
/// </summary>
/// <param name="context">The agent associated with the context menu.</param>
internal MenuTargetInventory(AgentInventoryContext* context)
{
this.Context = context;
}

/// <summary>
/// Gets the target item.
/// </summary>
public GameInventoryItem? TargetItem
{
get
{
var target = this.Context->TargetInventorySlot;
if (target != null)
return new(*target);
return null;
}
}

private AgentInventoryContext* Context { get; }
}
Loading

0 comments on commit 5f62c70

Please sign in to comment.