mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
599 lines
22 KiB
C#
599 lines
22 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
|
|
using Dalamud.Game.Text;
|
|
using Dalamud.Game.Text.SeStringHandling;
|
|
using Dalamud.Hooking;
|
|
using Dalamud.IoC;
|
|
using Dalamud.IoC.Internal;
|
|
using Dalamud.Logging.Internal;
|
|
using Dalamud.Memory;
|
|
using Dalamud.Plugin.Services;
|
|
using Dalamud.Utility;
|
|
|
|
using FFXIVClientStructs.FFXIV.Client.System.Memory;
|
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
|
|
|
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
|
|
|
|
namespace Dalamud.Game.Gui.ContextMenu;
|
|
|
|
/// <summary>
|
|
/// This class handles interacting with the game's (right-click) context menu.
|
|
/// </summary>
|
|
[ServiceManager.EarlyLoadedService]
|
|
internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextMenu
|
|
{
|
|
private static readonly ModuleLog Log = new("ContextMenu");
|
|
|
|
private readonly Hook<AtkModuleVf22OpenAddonByAgentDelegate> atkModuleVf22OpenAddonByAgentHook;
|
|
private readonly Hook<AddonContextMenuOnMenuSelectedDelegate> addonContextMenuOnMenuSelectedHook;
|
|
|
|
private uint? addonContextSubNameId;
|
|
|
|
[ServiceManager.ServiceConstructor]
|
|
private ContextMenu()
|
|
{
|
|
var raptureAtkModuleVtable = (nint*)RaptureAtkModule.StaticVirtualTablePointer;
|
|
this.atkModuleVf22OpenAddonByAgentHook = Hook<AtkModuleVf22OpenAddonByAgentDelegate>.FromAddress(raptureAtkModuleVtable[22], this.AtkModuleVf22OpenAddonByAgentDetour);
|
|
this.addonContextMenuOnMenuSelectedHook = Hook<AddonContextMenuOnMenuSelectedDelegate>.FromAddress((nint)AddonContextMenu.StaticVirtualTablePointer->OnMenuSelected, this.AddonContextMenuOnMenuSelectedDetour);
|
|
|
|
this.atkModuleVf22OpenAddonByAgentHook.Enable();
|
|
this.addonContextMenuOnMenuSelectedHook.Enable();
|
|
}
|
|
|
|
private delegate ushort AtkModuleVf22OpenAddonByAgentDelegate(AtkModule* module, byte* addonName, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, bool a8);
|
|
|
|
private delegate bool AddonContextMenuOnMenuSelectedDelegate(AddonContextMenu* addon, int selectedIdx, byte a3);
|
|
|
|
private delegate ushort RaptureAtkModuleOpenAddonDelegate(RaptureAtkModule* a1, uint addonNameId, uint valueCount, AtkValue* values, AgentInterface* parentAgent, ulong unk, ushort parentAddonId, int unk2);
|
|
|
|
/// <inheritdoc/>
|
|
public event IContextMenu.OnMenuOpenedDelegate? OnMenuOpened;
|
|
|
|
private Dictionary<ContextMenuType, List<IMenuItem>> MenuItems { get; } = [];
|
|
|
|
private object MenuItemsLock { get; } = new();
|
|
|
|
private AgentInterface* SelectedAgent { get; set; }
|
|
|
|
private ContextMenuType? SelectedMenuType { get; set; }
|
|
|
|
private List<IMenuItem>? SelectedItems { get; set; }
|
|
|
|
private HashSet<nint> SelectedEventInterfaces { get; } = [];
|
|
|
|
private AtkUnitBase* SelectedParentAddon { get; set; }
|
|
|
|
// -1 -> -inf: native items
|
|
// 0 -> inf: selected items
|
|
private List<int> MenuCallbackIds { get; } = [];
|
|
|
|
private IReadOnlyList<IMenuItem>? SubmenuItems { get; set; }
|
|
|
|
private uint AddonContextSubNameId
|
|
{
|
|
get
|
|
{
|
|
if (this.addonContextSubNameId is not uint id)
|
|
{
|
|
id = checked((uint)RaptureAtkModule.Instance()->AddonNames.FindIndex(s => s.EqualToString("AddonContextSub")));
|
|
this.addonContextSubNameId = id;
|
|
}
|
|
|
|
return id;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
void IInternalDisposableService.DisposeService()
|
|
{
|
|
this.atkModuleVf22OpenAddonByAgentHook.Dispose();
|
|
this.addonContextMenuOnMenuSelectedHook.Dispose();
|
|
|
|
var manager = RaptureAtkUnitManager.Instance();
|
|
if (manager == null)
|
|
return;
|
|
|
|
var menu = manager->GetAddonByName("ContextMenu");
|
|
var submenu = manager->GetAddonByName("AddonContextSub");
|
|
if (menu == null || submenu == null)
|
|
return;
|
|
|
|
if (menu->IsVisible)
|
|
menu->FireCallbackInt(-1);
|
|
if (submenu->IsVisible)
|
|
submenu->FireCallbackInt(-1);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void AddMenuItem(ContextMenuType menuType, IMenuItem item)
|
|
{
|
|
lock (this.MenuItemsLock)
|
|
{
|
|
if (!this.MenuItems.TryGetValue(menuType, out var items))
|
|
this.MenuItems[menuType] = items = [];
|
|
items.Add(item);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public bool RemoveMenuItem(ContextMenuType menuType, IMenuItem item)
|
|
{
|
|
lock (this.MenuItemsLock)
|
|
{
|
|
if (!this.MenuItems.TryGetValue(menuType, out var items))
|
|
return false;
|
|
return items.Remove(item);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the name with the given prefix.
|
|
/// </summary>
|
|
/// <param name="menuItem">The menu item to prefix.</param>
|
|
/// <returns>The prefixed name.</returns>
|
|
internal SeString GetPrefixedName(IMenuItem menuItem) =>
|
|
menuItem.Prefix is { } prefix
|
|
? new SeStringBuilder()
|
|
.AddUiForeground($"{prefix.ToIconString()} ", menuItem.PrefixColor)
|
|
.Append(menuItem.Name)
|
|
.Build()
|
|
: menuItem.Name;
|
|
|
|
private AtkValue* ExpandContextMenuArray(Span<AtkValue> oldValues, int newSize)
|
|
{
|
|
// if the array has enough room, don't reallocate
|
|
if (oldValues.Length >= newSize)
|
|
return (AtkValue*)Unsafe.AsPointer(ref oldValues[0]);
|
|
|
|
var size = (sizeof(AtkValue) * newSize) + 8;
|
|
var newArray = (nint)IMemorySpace.GetUISpace()->Malloc((ulong)size, 0);
|
|
if (newArray == nint.Zero)
|
|
throw new OutOfMemoryException();
|
|
NativeMemory.Fill((void*)newArray, (nuint)size, 0);
|
|
|
|
*(ulong*)newArray = (ulong)newSize;
|
|
|
|
// copy old memory if existing
|
|
if (!oldValues.IsEmpty)
|
|
oldValues.CopyTo(new((void*)(newArray + 8), oldValues.Length));
|
|
|
|
return (AtkValue*)(newArray + 8);
|
|
}
|
|
|
|
private void FreeExpandedContextMenuArray(AtkValue* newValues, int newSize) =>
|
|
IMemorySpace.Free((void*)((nint)newValues - 8), (ulong)((newSize * sizeof(AtkValue)) + 8));
|
|
|
|
private AtkValue* CreateEmptySubmenuContextMenuArray(SeString name, int x, int y, out int valueCount)
|
|
{
|
|
// 0: UInt = ContextItemCount
|
|
// 1: String = Name
|
|
// 2: Int = PositionX
|
|
// 3: Int = PositionY
|
|
// 4: Bool = false
|
|
// 5: UInt = ContextItemSubmenuMask
|
|
// 6: UInt = ReturnArrowMask (_gap_0x6BC ? 1 << (ContextItemCount - 1) : 0)
|
|
// 7: UInt = 1
|
|
|
|
valueCount = 8;
|
|
var values = this.ExpandContextMenuArray([], valueCount);
|
|
values[0].ChangeType(ValueType.UInt);
|
|
values[0].UInt = 0;
|
|
values[1].ChangeType(ValueType.String);
|
|
values[1].SetManagedString(name.Encode().NullTerminate());
|
|
values[2].ChangeType(ValueType.Int);
|
|
values[2].Int = x;
|
|
values[3].ChangeType(ValueType.Int);
|
|
values[3].Int = y;
|
|
values[4].ChangeType(ValueType.Bool);
|
|
values[4].Byte = 0;
|
|
values[5].ChangeType(ValueType.UInt);
|
|
values[5].UInt = 0;
|
|
values[6].ChangeType(ValueType.UInt);
|
|
values[6].UInt = 0;
|
|
values[7].ChangeType(ValueType.UInt);
|
|
values[7].UInt = 1;
|
|
return values;
|
|
}
|
|
|
|
private void SetupGenericMenu(int headerCount, int sizeHeaderIdx, int returnHeaderIdx, int submenuHeaderIdx, IReadOnlyList<IMenuItem> items, ref int valueCount, ref AtkValue* values)
|
|
{
|
|
var itemsWithIdx = items.Select((item, idx) => (item, idx)).OrderBy(i => i.item.Priority).ToArray();
|
|
var prefixItems = itemsWithIdx.Where(i => i.item.Priority < 0).ToArray();
|
|
var suffixItems = itemsWithIdx.Where(i => i.item.Priority >= 0).ToArray();
|
|
|
|
var nativeMenuSize = (int)values[sizeHeaderIdx].UInt;
|
|
var prefixMenuSize = prefixItems.Length;
|
|
var suffixMenuSize = suffixItems.Length;
|
|
|
|
var hasGameDisabled = valueCount - headerCount - nativeMenuSize > 0;
|
|
|
|
var hasCustomDisabled = items.Any(item => !item.IsEnabled);
|
|
var hasAnyDisabled = hasGameDisabled || hasCustomDisabled;
|
|
|
|
values = this.ExpandContextMenuArray(
|
|
new(values, valueCount),
|
|
valueCount = (nativeMenuSize + items.Count) * (hasAnyDisabled ? 2 : 1) + headerCount);
|
|
var offsetData = new Span<AtkValue>(values, headerCount);
|
|
var nameData = new Span<AtkValue>(values + headerCount, nativeMenuSize + items.Count);
|
|
var disabledData = hasAnyDisabled ? new Span<AtkValue>(values + headerCount + nativeMenuSize + items.Count, nativeMenuSize + items.Count) : [];
|
|
|
|
var returnMask = offsetData[returnHeaderIdx].UInt;
|
|
var submenuMask = offsetData[submenuHeaderIdx].UInt;
|
|
|
|
nameData[..nativeMenuSize].CopyTo(nameData.Slice(prefixMenuSize, nativeMenuSize));
|
|
if (hasAnyDisabled)
|
|
{
|
|
if (hasGameDisabled)
|
|
{
|
|
// copy old disabled data
|
|
var oldDisabledData = new Span<AtkValue>(values + headerCount + nativeMenuSize, nativeMenuSize);
|
|
oldDisabledData.CopyTo(disabledData.Slice(prefixMenuSize, nativeMenuSize));
|
|
}
|
|
else
|
|
{
|
|
// enable all
|
|
for (var i = prefixMenuSize; i < prefixMenuSize + nativeMenuSize; ++i)
|
|
{
|
|
disabledData[i].ChangeType(ValueType.Int);
|
|
disabledData[i].Int = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
returnMask <<= prefixMenuSize;
|
|
submenuMask <<= prefixMenuSize;
|
|
|
|
void FillData(Span<AtkValue> disabledData, Span<AtkValue> nameData, int i, IMenuItem item, int idx)
|
|
{
|
|
this.MenuCallbackIds.Add(idx);
|
|
|
|
if (hasAnyDisabled)
|
|
{
|
|
disabledData[i].ChangeType(ValueType.Int);
|
|
disabledData[i].Int = item.IsEnabled ? 0 : 1;
|
|
}
|
|
|
|
if (item.IsReturn)
|
|
returnMask |= 1u << i;
|
|
if (item.IsSubmenu)
|
|
submenuMask |= 1u << i;
|
|
|
|
nameData[i].ChangeType(ValueType.String);
|
|
nameData[i].SetManagedString(this.GetPrefixedName(item).Encode().NullTerminate());
|
|
}
|
|
|
|
for (var i = 0; i < prefixMenuSize; ++i)
|
|
{
|
|
var (item, idx) = prefixItems[i];
|
|
FillData(disabledData, nameData, i, item, idx);
|
|
}
|
|
|
|
this.MenuCallbackIds.AddRange(Enumerable.Range(0, nativeMenuSize).Select(i => -i - 1));
|
|
|
|
for (var i = prefixMenuSize + nativeMenuSize; i < prefixMenuSize + nativeMenuSize + suffixMenuSize; ++i)
|
|
{
|
|
var (item, idx) = suffixItems[i - prefixMenuSize - nativeMenuSize];
|
|
FillData(disabledData, nameData, i, item, idx);
|
|
}
|
|
|
|
offsetData[returnHeaderIdx].UInt = returnMask;
|
|
offsetData[submenuHeaderIdx].UInt = submenuMask;
|
|
|
|
offsetData[sizeHeaderIdx].UInt += (uint)items.Count;
|
|
}
|
|
|
|
private void SetupContextMenu(IReadOnlyList<IMenuItem> items, ref int valueCount, ref AtkValue* values)
|
|
{
|
|
// 0: UInt = Item Count
|
|
// 1: UInt = 0 (probably window name, just unused)
|
|
// 2: UInt = Return Mask (?)
|
|
// 3: UInt = Submenu Mask
|
|
// 4: UInt = OpenAtCursorPosition ? 2 : 1
|
|
// 5: UInt = 0
|
|
// 6: UInt = 0
|
|
|
|
foreach (var item in items)
|
|
{
|
|
if (!item.Prefix.HasValue)
|
|
{
|
|
item.Prefix = IMenuItem.DalamudDefaultPrefix;
|
|
item.PrefixColor = IMenuItem.DalamudDefaultPrefixColor;
|
|
|
|
if (!item.UseDefaultPrefix)
|
|
{
|
|
Log.Warning($"Menu item \"{item.Name}\" has no prefix, defaulting to Dalamud's. Menu items outside of a submenu must have a prefix.");
|
|
}
|
|
}
|
|
}
|
|
|
|
this.SetupGenericMenu(7, 0, 2, 3, items, ref valueCount, ref values);
|
|
}
|
|
|
|
private void SetupContextSubMenu(IReadOnlyList<IMenuItem> items, ref int valueCount, ref AtkValue* values)
|
|
{
|
|
// 0: UInt = ContextItemCount
|
|
// 1: skipped?
|
|
// 2: Int = PositionX
|
|
// 3: Int = PositionY
|
|
// 4: Bool = false
|
|
// 5: UInt = ContextItemSubmenuMask
|
|
// 6: UInt = _gap_0x6BC ? 1 << (ContextItemCount - 1) : 0
|
|
// 7: UInt = 1
|
|
|
|
this.SetupGenericMenu(8, 0, 6, 5, items, ref valueCount, ref values);
|
|
}
|
|
|
|
private ushort AtkModuleVf22OpenAddonByAgentDetour(AtkModule* module, byte* addonName, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, bool a8)
|
|
{
|
|
var oldValues = values;
|
|
|
|
if (MemoryHelper.EqualsZeroTerminatedString("ContextMenu", (nint)addonName))
|
|
{
|
|
this.MenuCallbackIds.Clear();
|
|
this.SelectedAgent = agent;
|
|
var unitManager = RaptureAtkUnitManager.Instance();
|
|
this.SelectedParentAddon = unitManager->GetAddonById(unitManager->GetAddonByName(addonName)->ContextMenuParentId);
|
|
this.SelectedEventInterfaces.Clear();
|
|
if (this.SelectedAgent == AgentInventoryContext.Instance())
|
|
{
|
|
this.SelectedMenuType = ContextMenuType.Inventory;
|
|
}
|
|
else if (this.SelectedAgent == AgentContext.Instance())
|
|
{
|
|
this.SelectedMenuType = ContextMenuType.Default;
|
|
|
|
var menu = AgentContext.Instance()->CurrentContextMenu;
|
|
var handlers = menu->EventHandlers;
|
|
var ids = menu->EventIds;
|
|
var count = (int)values[0].UInt;
|
|
handlers = handlers.Slice(7, count);
|
|
ids = ids.Slice(7, count);
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
if (ids[i] <= 106)
|
|
continue;
|
|
this.SelectedEventInterfaces.Add((nint)handlers[i].Value);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
this.SelectedMenuType = null;
|
|
}
|
|
|
|
this.SubmenuItems = null;
|
|
|
|
if (this.SelectedMenuType is { } menuType)
|
|
{
|
|
lock (this.MenuItemsLock)
|
|
{
|
|
if (this.MenuItems.TryGetValue(menuType, out var items))
|
|
this.SelectedItems = new(items);
|
|
else
|
|
this.SelectedItems = [];
|
|
}
|
|
|
|
var args = new MenuOpenedArgs(this.SelectedItems.Add, this.SelectedParentAddon, this.SelectedAgent, this.SelectedMenuType.Value, this.SelectedEventInterfaces);
|
|
this.OnMenuOpened?.InvokeSafely(args);
|
|
this.SelectedItems = this.FixupMenuList(this.SelectedItems, (int)values[0].UInt);
|
|
this.SetupContextMenu(this.SelectedItems, ref valueCount, ref values);
|
|
Log.Verbose($"Opening {this.SelectedMenuType} context menu with {this.SelectedItems.Count} custom items.");
|
|
}
|
|
else
|
|
{
|
|
this.SelectedItems = null;
|
|
}
|
|
}
|
|
else if (MemoryHelper.EqualsZeroTerminatedString("AddonContextSub", (nint)addonName))
|
|
{
|
|
this.MenuCallbackIds.Clear();
|
|
if (this.SubmenuItems != null)
|
|
{
|
|
this.SubmenuItems = this.FixupMenuList(this.SubmenuItems.ToList(), (int)values[0].UInt);
|
|
|
|
this.SetupContextSubMenu(this.SubmenuItems, ref valueCount, ref values);
|
|
Log.Verbose($"Opening {this.SelectedMenuType} submenu with {this.SubmenuItems.Count} custom items.");
|
|
}
|
|
}
|
|
else if (MemoryHelper.EqualsZeroTerminatedString("AddonContextMenuTitle", (nint)addonName))
|
|
{
|
|
this.MenuCallbackIds.Clear();
|
|
}
|
|
|
|
var ret = this.atkModuleVf22OpenAddonByAgentHook.Original(module, addonName, valueCount, values, agent, a7, a8);
|
|
if (values != oldValues)
|
|
this.FreeExpandedContextMenuArray(values, valueCount);
|
|
return ret;
|
|
}
|
|
|
|
private List<IMenuItem> FixupMenuList(List<IMenuItem> items, int nativeMenuSize)
|
|
{
|
|
// The in game menu actually supports 32 items, but the last item can't have a visible submenu arrow.
|
|
// As such, we'll only work with 31 items.
|
|
const int maxMenuItems = 31;
|
|
if (items.Count + nativeMenuSize > maxMenuItems)
|
|
{
|
|
Log.Warning($"Menu size exceeds {maxMenuItems} items, truncating.");
|
|
var orderedItems = items.OrderBy(i => i.Priority).ToArray();
|
|
var newItems = orderedItems[..(maxMenuItems - nativeMenuSize - 1)];
|
|
var submenuItems = orderedItems[(maxMenuItems - nativeMenuSize - 1)..];
|
|
return newItems.Append(new MenuItem
|
|
{
|
|
Prefix = SeIconChar.BoxedLetterD,
|
|
PrefixColor = 539,
|
|
IsSubmenu = true,
|
|
Priority = int.MaxValue,
|
|
Name = $"See More ({submenuItems.Length})",
|
|
OnClicked = a => a.OpenSubmenu(submenuItems),
|
|
}).ToList();
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
private void OpenSubmenu(SeString name, IReadOnlyList<IMenuItem> submenuItems, int posX, int posY)
|
|
{
|
|
if (submenuItems.Count == 0)
|
|
throw new ArgumentException("Submenu must not be empty", nameof(submenuItems));
|
|
|
|
this.SubmenuItems = submenuItems;
|
|
|
|
var module = RaptureAtkModule.Instance();
|
|
var values = this.CreateEmptySubmenuContextMenuArray(name, posX, posY, out var valueCount);
|
|
|
|
switch (this.SelectedMenuType)
|
|
{
|
|
case ContextMenuType.Default:
|
|
{
|
|
var ownerAddonId = ((AgentContext*)this.SelectedAgent)->OwnerAddon;
|
|
module->OpenAddon(this.AddonContextSubNameId, (uint)valueCount, values, &this.SelectedAgent->AtkEventInterface, 71, checked((ushort)ownerAddonId), 4);
|
|
break;
|
|
}
|
|
|
|
case ContextMenuType.Inventory:
|
|
{
|
|
var ownerAddonId = ((AgentInventoryContext*)this.SelectedAgent)->OwnerAddonId;
|
|
module->OpenAddon(this.AddonContextSubNameId, (uint)valueCount, values, &this.SelectedAgent->AtkEventInterface, 0, checked((ushort)ownerAddonId), 4);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
Log.Warning($"Unknown context menu type (agent: {(nint)this.SelectedAgent}, cannot open submenu");
|
|
break;
|
|
}
|
|
|
|
this.FreeExpandedContextMenuArray(values, valueCount);
|
|
}
|
|
|
|
private bool AddonContextMenuOnMenuSelectedDetour(AddonContextMenu* addon, int selectedIdx, byte a3)
|
|
{
|
|
var items = this.SubmenuItems ?? this.SelectedItems;
|
|
if (items == null)
|
|
goto original;
|
|
if (this.MenuCallbackIds.Count == 0)
|
|
goto original;
|
|
if (selectedIdx < 0)
|
|
goto original;
|
|
if (selectedIdx >= this.MenuCallbackIds.Count)
|
|
goto original;
|
|
|
|
var callbackId = this.MenuCallbackIds[selectedIdx];
|
|
|
|
if (callbackId < 0)
|
|
{
|
|
selectedIdx = -callbackId - 1;
|
|
}
|
|
else
|
|
{
|
|
var item = items[callbackId];
|
|
var openedSubmenu = false;
|
|
|
|
try
|
|
{
|
|
if (item.OnClicked == null)
|
|
throw new InvalidOperationException("Item has no OnClicked handler");
|
|
item.OnClicked.InvokeSafely(new MenuItemClickedArgs(
|
|
(name, submenuItems) =>
|
|
{
|
|
short x, y;
|
|
addon->AtkUnitBase.GetPosition(&x, &y);
|
|
this.OpenSubmenu(name ?? item.Name, submenuItems, x, y);
|
|
openedSubmenu = true;
|
|
},
|
|
this.SelectedParentAddon,
|
|
this.SelectedAgent,
|
|
this.SelectedMenuType ?? default,
|
|
this.SelectedEventInterfaces));
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Error(e, "Error while handling context menu click");
|
|
}
|
|
|
|
// Close with click sound
|
|
if (!openedSubmenu)
|
|
addon->AtkUnitBase.FireCallbackInt(-2);
|
|
return false;
|
|
}
|
|
|
|
original:
|
|
// Eventually handled by inventory context here: 14022BBD0 (6.51)
|
|
return this.addonContextMenuOnMenuSelectedHook.Original(addon, selectedIdx, a3);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Plugin-scoped version of a <see cref="ContextMenu"/> service.
|
|
/// </summary>
|
|
[PluginInterface]
|
|
[ServiceManager.ScopedService]
|
|
#pragma warning disable SA1015
|
|
[ResolveVia<IContextMenu>]
|
|
#pragma warning restore SA1015
|
|
internal class ContextMenuPluginScoped : IInternalDisposableService, IContextMenu
|
|
{
|
|
[ServiceManager.ServiceDependency]
|
|
private readonly ContextMenu parentService = Service<ContextMenu>.Get();
|
|
|
|
private ContextMenuPluginScoped()
|
|
{
|
|
this.parentService.OnMenuOpened += this.OnMenuOpenedForward;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public event IContextMenu.OnMenuOpenedDelegate? OnMenuOpened;
|
|
|
|
private Dictionary<ContextMenuType, List<IMenuItem>> MenuItems { get; } = [];
|
|
|
|
private object MenuItemsLock { get; } = new();
|
|
|
|
/// <inheritdoc/>
|
|
void IInternalDisposableService.DisposeService()
|
|
{
|
|
this.parentService.OnMenuOpened -= this.OnMenuOpenedForward;
|
|
|
|
this.OnMenuOpened = null;
|
|
|
|
lock (this.MenuItemsLock)
|
|
{
|
|
foreach (var (menuType, items) in this.MenuItems)
|
|
{
|
|
foreach (var item in items)
|
|
this.parentService.RemoveMenuItem(menuType, item);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void AddMenuItem(ContextMenuType menuType, IMenuItem item)
|
|
{
|
|
lock (this.MenuItemsLock)
|
|
{
|
|
if (!this.MenuItems.TryGetValue(menuType, out var items))
|
|
this.MenuItems[menuType] = items = [];
|
|
items.Add(item);
|
|
}
|
|
|
|
this.parentService.AddMenuItem(menuType, item);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public bool RemoveMenuItem(ContextMenuType menuType, IMenuItem item)
|
|
{
|
|
lock (this.MenuItemsLock)
|
|
{
|
|
if (this.MenuItems.TryGetValue(menuType, out var items))
|
|
items.Remove(item);
|
|
}
|
|
|
|
return this.parentService.RemoveMenuItem(menuType, item);
|
|
}
|
|
|
|
private void OnMenuOpenedForward(IMenuOpenedArgs args) =>
|
|
this.OnMenuOpened?.Invoke(args);
|
|
}
|