Context menu UI backend refactor & better UX (#13318)

closes https://github.com/space-wizards/space-station-14/issues/9209
This commit is contained in:
Kara
2023-01-07 21:24:52 -06:00
committed by GitHub
parent 17be16f1b1
commit 45da85fec6
14 changed files with 218 additions and 187 deletions

View File

@@ -2,10 +2,12 @@ using System.Linq;
using Content.Client.Administration.Systems; using Content.Client.Administration.Systems;
using Content.Client.UserInterface.Controls; using Content.Client.UserInterface.Controls;
using Content.Client.Verbs; using Content.Client.Verbs;
using Content.Client.Verbs.UI;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.Input; using Content.Shared.Input;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Shared.Input; using Robust.Shared.Input;
@@ -56,7 +58,7 @@ namespace Content.Client.Administration.UI.CustomControls
} }
else if (args.Event.Function == EngineKeyFunctions.UseSecondary && selectedPlayer.EntityUid != null) else if (args.Event.Function == EngineKeyFunctions.UseSecondary && selectedPlayer.EntityUid != null)
{ {
_verbSystem.VerbMenu.OpenVerbMenu(selectedPlayer.EntityUid.Value); IoCManager.Resolve<IUserInterfaceManager>().GetUIController<VerbMenuUIController>().OpenVerbMenu(selectedPlayer.EntityUid.Value);
} }
} }

View File

@@ -1,7 +1,9 @@
using Content.Client.ContextMenu.UI;
using Content.Client.Verbs; using Content.Client.Verbs;
using Content.Shared.CombatMode; using Content.Shared.CombatMode;
using Content.Shared.Targeting; using Content.Shared.Targeting;
using Robust.Client.Player; using Robust.Client.Player;
using Robust.Client.UserInterface;
namespace Content.Client.CombatMode namespace Content.Client.CombatMode
{ {
@@ -38,8 +40,7 @@ namespace Content.Client.CombatMode
return; return;
} }
var verbs = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<VerbSystem>(); IoCManager.Resolve<IUserInterfaceManager>().GetUIController<ContextMenuUIController>().Close();
verbs.CloseAllMenus();
} }
} }
} }

View File

@@ -12,7 +12,7 @@ namespace Content.Client.Commands
public string Description => "Sets the entity menu grouping type."; public string Description => "Sets the entity menu grouping type.";
public string Help => $"Usage: entitymenug <0:{EntityMenuPresenter.GroupingTypesCount}>"; public string Help => $"Usage: entitymenug <0:{EntityMenuUIController.GroupingTypesCount}>";
public void Execute(IConsoleShell shell, string argStr, string[] args) public void Execute(IConsoleShell shell, string argStr, string[] args)
{ {
if (args.Length != 1) if (args.Length != 1)
@@ -27,7 +27,7 @@ namespace Content.Client.Commands
return; return;
} }
if (id < 0 ||id > EntityMenuPresenter.GroupingTypesCount - 1) if (id < 0 ||id > EntityMenuUIController.GroupingTypesCount - 1)
{ {
shell.WriteLine($"{args[0]} is not a valid integer."); shell.WriteLine($"{args[0]} is not a valid integer.");
return; return;

View File

@@ -31,14 +31,14 @@ namespace Content.Client.ContextMenu.UI
/// </summary> /// </summary>
public GridContainer MenuBody = new(); public GridContainer MenuBody = new();
private ContextMenuPresenter _presenter; private ContextMenuUIController _uiController;
public ContextMenuPopup (ContextMenuPresenter presenter, ContextMenuElement? parentElement) : base() public ContextMenuPopup (ContextMenuUIController uiController, ContextMenuElement? parentElement) : base()
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
MenuPanel.SetOnlyStyleClass(StyleClassContextMenuPopup); MenuPanel.SetOnlyStyleClass(StyleClassContextMenuPopup);
_presenter = presenter; _uiController = uiController;
ParentElement = parentElement; ParentElement = parentElement;
// TODO xaml controls now have the access options -> re-xamlify all this. // TODO xaml controls now have the access options -> re-xamlify all this.
@@ -52,7 +52,7 @@ namespace Content.Client.ContextMenu.UI
MenuPanel.MaxHeight = MaxItemsBeforeScroll * (ContextMenuElement.ElementHeight + 2 * ContextMenuElement.ElementMargin) + styleSize.Y; MenuPanel.MaxHeight = MaxItemsBeforeScroll * (ContextMenuElement.ElementHeight + 2 * ContextMenuElement.ElementMargin) + styleSize.Y;
UserInterfaceManager.ModalRoot.AddChild(this); UserInterfaceManager.ModalRoot.AddChild(this);
MenuBody.OnChildRemoved += ctrl => _presenter.OnRemoveElement(this, ctrl); MenuBody.OnChildRemoved += ctrl => _uiController.OnRemoveElement(this, ctrl);
MenuBody.VSeparationOverride = 0; MenuBody.VSeparationOverride = 0;
MenuBody.HSeparationOverride = 0; MenuBody.HSeparationOverride = 0;
@@ -67,13 +67,13 @@ namespace Content.Client.ContextMenu.UI
OnPopupHide += () => OnPopupHide += () =>
{ {
if (ParentElement != null) if (ParentElement != null)
_presenter.CloseSubMenus(ParentElement.ParentMenu); _uiController.CloseSubMenus(ParentElement.ParentMenu);
}; };
} }
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
MenuBody.OnChildRemoved -= ctrl => _presenter.OnRemoveElement(this, ctrl); MenuBody.OnChildRemoved -= ctrl => _uiController.OnRemoveElement(this, ctrl);
ParentElement = null; ParentElement = null;
base.Dispose(disposing); base.Dispose(disposing);
} }

View File

@@ -1,24 +1,26 @@
using System;
using System.Collections.Generic;
using System.Threading; using System.Threading;
using Content.Client.Gameplay;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Shared.Log; using Robust.Client.UserInterface.Controllers;
using Robust.Shared.Maths;
using Timer = Robust.Shared.Timing.Timer; using Timer = Robust.Shared.Timing.Timer;
namespace Content.Client.ContextMenu.UI namespace Content.Client.ContextMenu.UI
{ {
/// <summary> /// <summary>
/// This class handles all the logic associated with showing a context menu. /// This class handles all the logic associated with showing a context menu, as well as all the state for the
/// entire context menu stack, including verb and entity menus. It does not currently support multiple
/// open context menus.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This largely involves setting up timers to open and close sub-menus when hovering over other menu elements. /// This largely involves setting up timers to open and close sub-menus when hovering over other menu elements.
/// </remarks> /// </remarks>
[Virtual] public sealed class ContextMenuUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>
public class ContextMenuPresenter : IDisposable
{ {
public static readonly TimeSpan HoverDelay = TimeSpan.FromSeconds(0.2); public static readonly TimeSpan HoverDelay = TimeSpan.FromSeconds(0.2);
public ContextMenuPopup RootMenu; /// <summary>
/// Root menu of the entire context menu.
/// </summary>
public ContextMenuPopup RootMenu = default!;
public Stack<ContextMenuPopup> Menus { get; } = new(); public Stack<ContextMenuPopup> Menus { get; } = new();
/// <summary> /// <summary>
@@ -31,30 +33,35 @@ namespace Content.Client.ContextMenu.UI
/// </summary> /// </summary>
public CancellationTokenSource? CancelClose; public CancellationTokenSource? CancelClose;
public ContextMenuPresenter() public Action? OnContextClosed;
public Action<ContextMenuElement>? OnContextMouseEntered;
public Action<ContextMenuElement>? OnContextMouseExited;
public Action<ContextMenuElement>? OnSubMenuOpened;
public Action<ContextMenuElement, GUIBoundKeyEventArgs>? OnContextKeyEvent;
public void OnStateEntered(GameplayState state)
{ {
RootMenu = new(this, null); RootMenu = new(this, null);
RootMenu.OnPopupHide += RootMenu.MenuBody.DisposeAllChildren; RootMenu.OnPopupHide += Close;
Menus.Push(RootMenu); Menus.Push(RootMenu);
} }
/// <summary> public void OnStateExited(GameplayState state)
/// Dispose of all UI elements.
/// </summary>
public virtual void Dispose()
{ {
RootMenu.OnPopupHide -= RootMenu.MenuBody.DisposeAllChildren; Close();
RootMenu.OnPopupHide -= Close;
RootMenu.Dispose(); RootMenu.Dispose();
} }
/// <summary> /// <summary>
/// Close and clear the root menu. This will also dispose any sub-menus. /// Close and clear the root menu. This will also dispose any sub-menus.
/// </summary> /// </summary>
public virtual void Close() public void Close()
{ {
RootMenu.Close(); RootMenu.MenuBody.DisposeAllChildren();
CancelOpen?.Cancel(); CancelOpen?.Cancel();
CancelClose?.Cancel(); CancelClose?.Cancel();
OnContextClosed?.Invoke();
} }
/// <summary> /// <summary>
@@ -82,7 +89,7 @@ namespace Content.Client.ContextMenu.UI
/// <summary> /// <summary>
/// Start a timer to open this element's sub-menu. /// Start a timer to open this element's sub-menu.
/// </summary> /// </summary>
public virtual void OnMouseEntered(ContextMenuElement element) private void OnMouseEntered(ContextMenuElement element)
{ {
if (!Menus.TryPeek(out var topMenu)) if (!Menus.TryPeek(out var topMenu))
{ {
@@ -100,6 +107,7 @@ namespace Content.Client.ContextMenu.UI
CancelOpen?.Cancel(); CancelOpen?.Cancel();
CancelOpen = new(); CancelOpen = new();
Timer.Spawn(HoverDelay, () => OpenSubMenu(element), CancelOpen.Token); Timer.Spawn(HoverDelay, () => OpenSubMenu(element), CancelOpen.Token);
OnContextMouseEntered?.Invoke(element);
} }
/// <summary> /// <summary>
@@ -108,7 +116,7 @@ namespace Content.Client.ContextMenu.UI
/// <remarks> /// <remarks>
/// Note that this timer will be aborted when entering the actual sub-menu itself. /// Note that this timer will be aborted when entering the actual sub-menu itself.
/// </remarks> /// </remarks>
public virtual void OnMouseExited(ContextMenuElement element) private void OnMouseExited(ContextMenuElement element)
{ {
CancelOpen?.Cancel(); CancelOpen?.Cancel();
@@ -118,9 +126,13 @@ namespace Content.Client.ContextMenu.UI
CancelClose?.Cancel(); CancelClose?.Cancel();
CancelClose = new(); CancelClose = new();
Timer.Spawn(HoverDelay, () => CloseSubMenus(element.ParentMenu), CancelClose.Token); Timer.Spawn(HoverDelay, () => CloseSubMenus(element.ParentMenu), CancelClose.Token);
OnContextMouseExited?.Invoke(element);
} }
public virtual void OnKeyBindDown(ContextMenuElement element, GUIBoundKeyEventArgs args) { } private void OnKeyBindDown(ContextMenuElement element, GUIBoundKeyEventArgs args)
{
OnContextKeyEvent?.Invoke(element, args);
}
/// <summary> /// <summary>
/// Opens a new sub menu, and close the old one. /// Opens a new sub menu, and close the old one.
@@ -128,7 +140,7 @@ namespace Content.Client.ContextMenu.UI
/// <remarks> /// <remarks>
/// If the given element has no sub-menu, just close the current one. /// If the given element has no sub-menu, just close the current one.
/// </remarks> /// </remarks>
public virtual void OpenSubMenu(ContextMenuElement element) public void OpenSubMenu(ContextMenuElement element)
{ {
if (!Menus.TryPeek(out var topMenu)) if (!Menus.TryPeek(out var topMenu))
{ {
@@ -164,6 +176,7 @@ namespace Content.Client.ContextMenu.UI
element.SubMenu.SetPositionLast(); element.SubMenu.SetPositionLast();
Menus.Push(element.SubMenu); Menus.Push(element.SubMenu);
OnSubMenuOpened?.Invoke(element);
} }
/// <summary> /// <summary>

View File

@@ -45,13 +45,13 @@ namespace Content.Client.ContextMenu.UI
LayoutContainer.SetGrowVertical(CountLabel, LayoutContainer.GrowDirection.Begin); LayoutContainer.SetGrowVertical(CountLabel, LayoutContainer.GrowDirection.Begin);
Entity = entity; Entity = entity;
if (Entity != null) if (Entity == null)
{ return;
Count = 1; Count = 1;
CountLabel.Visible = false; CountLabel.Visible = false;
UpdateEntity(); UpdateEntity();
} }
}
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {

View File

@@ -1,16 +1,17 @@
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using System.Linq; using System.Linq;
using Robust.Client.UserInterface.Controllers;
namespace Content.Client.ContextMenu.UI namespace Content.Client.ContextMenu.UI
{ {
public sealed partial class EntityMenuPresenter : ContextMenuPresenter public sealed partial class EntityMenuUIController
{ {
public const int GroupingTypesCount = 2; public const int GroupingTypesCount = 2;
private int GroupingContextMenuType { get; set; } private int GroupingContextMenuType { get; set; }
public void OnGroupingChanged(int obj) public void OnGroupingChanged(int obj)
{ {
Close(); _context.Close();
GroupingContextMenuType = obj; GroupingContextMenuType = obj;
} }

View File

@@ -1,10 +1,9 @@
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Content.Client.CombatMode; using Content.Client.CombatMode;
using Content.Client.Examine; using Content.Client.Examine;
using Content.Client.Gameplay; using Content.Client.Gameplay;
using Content.Client.Verbs; using Content.Client.Verbs;
using Content.Client.Viewport; using Content.Client.Verbs.UI;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.CombatMode; using Content.Shared.CombatMode;
using Content.Shared.Examine; using Content.Shared.Examine;
@@ -15,14 +14,11 @@ using Robust.Client.Input;
using Robust.Client.Player; using Robust.Client.Player;
using Robust.Client.State; using Robust.Client.State;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Input; using Robust.Shared.Input;
using Robust.Shared.Input.Binding; using Robust.Shared.Input.Binding;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing; using Robust.Shared.Timing;
namespace Content.Client.ContextMenu.UI namespace Content.Client.ContextMenu.UI
@@ -31,11 +27,11 @@ namespace Content.Client.ContextMenu.UI
/// This class handles the displaying of the entity context menu. /// This class handles the displaying of the entity context menu.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// In addition to the normal <see cref="ContextMenuPresenter"/> functionality, this also provides functions get /// This also provides functions to get
/// a list of entities near the mouse position, add them to the context menu grouped by prototypes, and remove /// a list of entities near the mouse position, add them to the context menu grouped by prototypes, and remove
/// them from the menu as they move out of sight. /// them from the menu as they move out of sight.
/// </remarks> /// </remarks>
public sealed partial class EntityMenuPresenter : ContextMenuPresenter public sealed partial class EntityMenuUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>
{ {
[Dependency] private readonly IEntitySystemManager _systemManager = default!; [Dependency] private readonly IEntitySystemManager _systemManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!;
@@ -46,11 +42,15 @@ namespace Content.Client.ContextMenu.UI
[Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!; [Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly ContextMenuUIController _context = default!;
[Dependency] private readonly VerbMenuUIController _verb = default!;
private readonly VerbSystem _verbSystem; [UISystemDependency] private readonly VerbSystem _verbSystem = default!;
private readonly ExamineSystem _examineSystem; [UISystemDependency] private readonly ExamineSystem _examineSystem = default!;
private readonly TransformSystem _xform; [UISystemDependency] private readonly TransformSystem _xform = default!;
private readonly SharedCombatModeSystem _combatMode; [UISystemDependency] private readonly CombatModeSystem _combatMode = default!;
private bool _updating;
/// <summary> /// <summary>
/// This maps the currently displayed entities to the actual GUI elements. /// This maps the currently displayed entities to the actual GUI elements.
@@ -60,27 +60,26 @@ namespace Content.Client.ContextMenu.UI
/// </remarks> /// </remarks>
public Dictionary<EntityUid, EntityMenuElement> Elements = new(); public Dictionary<EntityUid, EntityMenuElement> Elements = new();
public EntityMenuPresenter(VerbSystem verbSystem) : base() public void OnStateEntered(GameplayState state)
{ {
IoCManager.InjectDependencies(this); _updating = true;
_verbSystem = verbSystem;
_examineSystem = _entityManager.EntitySysManager.GetEntitySystem<ExamineSystem>();
_combatMode = _entityManager.EntitySysManager.GetEntitySystem<CombatModeSystem>();
_xform = _entityManager.EntitySysManager.GetEntitySystem<TransformSystem>();
_cfg.OnValueChanged(CCVars.EntityMenuGroupingType, OnGroupingChanged, true); _cfg.OnValueChanged(CCVars.EntityMenuGroupingType, OnGroupingChanged, true);
_context.OnContextMouseEntered += OnMouseEntered;
_context.OnContextKeyEvent += OnKeyBindDown;
CommandBinds.Builder CommandBinds.Builder
.Bind(EngineKeyFunctions.UseSecondary, new PointerInputCmdHandler(HandleOpenEntityMenu, outsidePrediction: true)) .Bind(EngineKeyFunctions.UseSecondary, new PointerInputCmdHandler(HandleOpenEntityMenu, outsidePrediction: true))
.Register<EntityMenuPresenter>(); .Register<EntityMenuUIController>();
} }
public override void Dispose() public void OnStateExited(GameplayState state)
{ {
base.Dispose(); _updating = false;
Elements.Clear(); Elements.Clear();
CommandBinds.Unregister<EntityMenuPresenter>(); _cfg.UnsubValueChanged(CCVars.EntityMenuGroupingType, OnGroupingChanged);
_context.OnContextMouseEntered -= OnMouseEntered;
_context.OnContextKeyEvent -= OnKeyBindDown;
CommandBinds.Unregister<EntityMenuUIController>();
} }
/// <summary> /// <summary>
@@ -89,8 +88,8 @@ namespace Content.Client.ContextMenu.UI
public void OpenRootMenu(List<EntityUid> entities) public void OpenRootMenu(List<EntityUid> entities)
{ {
// close any old menus first. // close any old menus first.
if (RootMenu.Visible) if (_context.RootMenu.Visible)
Close(); _context.Close();
var entitySpriteStates = GroupEntities(entities); var entitySpriteStates = GroupEntities(entities);
var orderedStates = entitySpriteStates.ToList(); var orderedStates = entitySpriteStates.ToList();
@@ -99,12 +98,30 @@ namespace Content.Client.ContextMenu.UI
AddToUI(orderedStates); AddToUI(orderedStates);
var box = UIBox2.FromDimensions(_userInterfaceManager.MousePositionScaled.Position, (1, 1)); var box = UIBox2.FromDimensions(_userInterfaceManager.MousePositionScaled.Position, (1, 1));
RootMenu.Open(box); _context.RootMenu.Open(box);
} }
public override void OnKeyBindDown(ContextMenuElement element, GUIBoundKeyEventArgs args) public void OnMouseEntered(ContextMenuElement element)
{
if (element is not EntityMenuElement entityElement)
return;
// get an entity associated with this element
var entity = entityElement.Entity;
// if there is none, this is a group, so don't open verbs
if (entity == null)
return;
// Deleted() automatically checks for null & existence.
if (_entityManager.Deleted(entity))
return;
_verb.OpenVerbMenu(entity.Value, popup: element.SubMenu);
}
public void OnKeyBindDown(ContextMenuElement element, GUIBoundKeyEventArgs args)
{ {
base.OnKeyBindDown(element, args);
if (element is not EntityMenuElement entityElement) if (element is not EntityMenuElement entityElement)
return; return;
@@ -116,14 +133,6 @@ namespace Content.Client.ContextMenu.UI
if (_entityManager.Deleted(entity)) if (_entityManager.Deleted(entity))
return; return;
// open verb menu?
if (args.Function == EngineKeyFunctions.UseSecondary)
{
_verbSystem.VerbMenu.OpenVerbMenu(entity.Value);
args.Handle();
return;
}
// do examination? // do examination?
if (args.Function == ContentKeyFunctions.ExamineEntity) if (args.Function == ContentKeyFunctions.ExamineEntity)
{ {
@@ -154,9 +163,8 @@ namespace Content.Client.ContextMenu.UI
inputSys.HandleInputCommand(session, func, message); inputSys.HandleInputCommand(session, func, message);
} }
_verbSystem.CloseAllMenus(); _context.Close();
args.Handle(); args.Handle();
return;
} }
} }
@@ -182,9 +190,12 @@ namespace Content.Client.ContextMenu.UI
/// <summary> /// <summary>
/// Check that entities in the context menu are still visible. If not, remove them from the context menu. /// Check that entities in the context menu are still visible. If not, remove them from the context menu.
/// </summary> /// </summary>
public void Update() public override void FrameUpdate(FrameEventArgs args)
{ {
if (!RootMenu.Visible) if (!_updating || _context.RootMenu == null)
return;
if (!_context.RootMenu.Visible)
return; return;
if (_playerManager.LocalPlayer?.ControlledEntity is not { } player || if (_playerManager.LocalPlayer?.ControlledEntity is not { } player ||
@@ -229,7 +240,8 @@ namespace Content.Client.ContextMenu.UI
foreach (var entity in entityGroups[0]) foreach (var entity in entityGroups[0])
{ {
var element = new EntityMenuElement(entity); var element = new EntityMenuElement(entity);
AddElement(RootMenu, element); element.SubMenu = new ContextMenuPopup(_context, element);
_context.AddElement(_context.RootMenu, element);
Elements.TryAdd(entity, element); Elements.TryAdd(entity, element);
} }
return; return;
@@ -245,7 +257,8 @@ namespace Content.Client.ContextMenu.UI
// this group only has a single entity, add a simple menu element // this group only has a single entity, add a simple menu element
var element = new EntityMenuElement(group[0]); var element = new EntityMenuElement(group[0]);
AddElement(RootMenu, element); element.SubMenu = new ContextMenuPopup(_context, element);
_context.AddElement(_context.RootMenu, element);
Elements.TryAdd(group[0], element); Elements.TryAdd(group[0], element);
} }
@@ -257,17 +270,18 @@ namespace Content.Client.ContextMenu.UI
private void AddGroupToUI(List<EntityUid> group) private void AddGroupToUI(List<EntityUid> group)
{ {
EntityMenuElement element = new(); EntityMenuElement element = new();
ContextMenuPopup subMenu = new(this, element); ContextMenuPopup subMenu = new(_context, element);
foreach (var entity in group) foreach (var entity in group)
{ {
var subElement = new EntityMenuElement(entity); var subElement = new EntityMenuElement(entity);
AddElement(subMenu, subElement); subElement.SubMenu = new ContextMenuPopup(_context, subElement);
_context.AddElement(subMenu, subElement);
Elements.TryAdd(entity, subElement); Elements.TryAdd(entity, subElement);
} }
UpdateElement(element); UpdateElement(element);
AddElement(RootMenu, element); _context.AddElement(_context.RootMenu, element);
} }
/// <summary> /// <summary>
@@ -291,13 +305,9 @@ namespace Content.Client.ContextMenu.UI
if (parent is EntityMenuElement e) if (parent is EntityMenuElement e)
UpdateElement(e); UpdateElement(e);
// if the verb menu is open and targeting this entity, close it.
if (_verbSystem.VerbMenu.CurrentTarget == entity)
_verbSystem.VerbMenu.Close();
// If this was the last entity, close the entity menu // If this was the last entity, close the entity menu
if (RootMenu.MenuBody.ChildCount == 0) if (_context.RootMenu.MenuBody.ChildCount == 0)
Close(); _context.Close();
} }
/// <summary> /// <summary>
@@ -376,17 +386,5 @@ namespace Content.Client.ContextMenu.UI
return null; return null;
} }
public override void OpenSubMenu(ContextMenuElement element)
{
base.OpenSubMenu(element);
// In case the verb menu is currently open, ensure that it is shown ABOVE the entity menu.
if (_verbSystem.VerbMenu.Menus.TryPeek(out var menu) && menu.Visible)
{
menu.ParentElement?.ParentMenu?.SetPositionLast();
menu.SetPositionLast();
}
}
} }
} }

View File

@@ -14,6 +14,8 @@ using Robust.Shared.GameStates;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Content.Client.Verbs.UI;
using Robust.Client.UserInterface;
namespace Content.Client.Hands.Systems namespace Content.Client.Hands.Systems
{ {
@@ -22,11 +24,11 @@ namespace Content.Client.Hands.Systems
{ {
[Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IUserInterfaceManager _ui = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!; [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly StrippableSystem _stripSys = default!; [Dependency] private readonly StrippableSystem _stripSys = default!;
[Dependency] private readonly ExamineSystem _examine = default!; [Dependency] private readonly ExamineSystem _examine = default!;
[Dependency] private readonly VerbSystem _verbs = default!;
public event Action<string, HandLocation>? OnPlayerAddHand; public event Action<string, HandLocation>? OnPlayerAddHand;
public event Action<string>? OnPlayerRemoveHand; public event Action<string>? OnPlayerRemoveHand;
@@ -240,7 +242,7 @@ namespace Content.Client.Hands.Systems
return; return;
} }
_verbs.VerbMenu.OpenVerbMenu(entity); _ui.GetUIController<VerbMenuUIController>().OpenVerbMenu(entity);
} }
public void UIHandAltActivateItem(string handName) public void UIHandAltActivateItem(string handName)

View File

@@ -3,6 +3,7 @@ using Content.Client.Examine;
using Content.Client.Storage; using Content.Client.Storage;
using Content.Client.UserInterface.Controls; using Content.Client.UserInterface.Controls;
using Content.Client.Verbs; using Content.Client.Verbs;
using Content.Client.Verbs.UI;
using Content.Shared.Clothing.Components; using Content.Shared.Clothing.Components;
using Content.Shared.Hands.Components; using Content.Shared.Hands.Components;
using Content.Shared.Interaction; using Content.Shared.Interaction;
@@ -12,6 +13,7 @@ using Content.Shared.Inventory.Events;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Client.Player; using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.Input.Binding; using Robust.Shared.Input.Binding;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -23,10 +25,10 @@ namespace Content.Client.Inventory
{ {
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IUserInterfaceManager _ui = default!;
[Dependency] private readonly ClientClothingSystem _clothingVisualsSystem = default!; [Dependency] private readonly ClientClothingSystem _clothingVisualsSystem = default!;
[Dependency] private readonly ExamineSystem _examine = default!; [Dependency] private readonly ExamineSystem _examine = default!;
[Dependency] private readonly VerbSystem _verbs = default!;
public Action<SlotData>? EntitySlotUpdate = null; public Action<SlotData>? EntitySlotUpdate = null;
public Action<SlotData>? OnSlotAdded = null; public Action<SlotData>? OnSlotAdded = null;
@@ -270,7 +272,7 @@ namespace Content.Client.Inventory
if (!TryGetSlotEntity(uid, slot, out var item)) if (!TryGetSlotEntity(uid, slot, out var item))
return; return;
_verbs.VerbMenu.OpenVerbMenu(item.Value); _ui.GetUIController<VerbMenuUIController>().OpenVerbMenu(item.Value);
} }
public void UIInventoryActivateItem(string slot, EntityUid uid) public void UIInventoryActivateItem(string slot, EntityUid uid)

View File

@@ -2,6 +2,7 @@ using Content.Client.Examine;
using Content.Client.Storage.UI; using Content.Client.Storage.UI;
using Content.Client.UserInterface.Controls; using Content.Client.UserInterface.Controls;
using Content.Client.Verbs; using Content.Client.Verbs;
using Content.Client.Verbs.UI;
using Content.Shared.Input; using Content.Shared.Input;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using JetBrains.Annotations; using JetBrains.Annotations;
@@ -72,7 +73,7 @@ namespace Content.Client.Storage
} }
else if (args.Function == EngineKeyFunctions.UseSecondary) else if (args.Function == EngineKeyFunctions.UseSecondary)
{ {
entitySys.GetEntitySystem<VerbSystem>().VerbMenu.OpenVerbMenu(entity); IoCManager.Resolve<IUserInterfaceManager>().GetUIController<VerbMenuUIController>().OpenVerbMenu(entity);
} }
else if (args.Function == ContentKeyFunctions.ActivateItemInWorld) else if (args.Function == ContentKeyFunctions.ActivateItemInWorld)
{ {

View File

@@ -5,6 +5,7 @@ using Content.Client.Administration.UI.Tabs.PlayerTab;
using Content.Client.Gameplay; using Content.Client.Gameplay;
using Content.Client.UserInterface.Controls; using Content.Client.UserInterface.Controls;
using Content.Client.Verbs; using Content.Client.Verbs;
using Content.Client.Verbs.UI;
using Content.Shared.Input; using Content.Shared.Input;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Client.Console; using Robust.Client.Console;
@@ -26,8 +27,7 @@ public sealed class AdminUIController : UIController, IOnStateEntered<GameplaySt
[Dependency] private readonly IClientConGroupController _conGroups = default!; [Dependency] private readonly IClientConGroupController _conGroups = default!;
[Dependency] private readonly IClientConsoleHost _conHost = default!; [Dependency] private readonly IClientConsoleHost _conHost = default!;
[Dependency] private readonly IInputManager _input = default!; [Dependency] private readonly IInputManager _input = default!;
[Dependency] private readonly VerbMenuUIController _verb = default!;
[UISystemDependency] private readonly VerbSystem _verbs = default!;
private AdminMenuWindow? _window; private AdminMenuWindow? _window;
private MenuButton? AdminButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.AdminButton; private MenuButton? AdminButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.AdminButton;
@@ -135,7 +135,7 @@ public sealed class AdminUIController : UIController, IOnStateEntered<GameplaySt
if (function == EngineKeyFunctions.UIClick) if (function == EngineKeyFunctions.UIClick)
_conHost.ExecuteCommand($"vv {uid}"); _conHost.ExecuteCommand($"vv {uid}");
else if (function == EngineKeyFunctions.UseSecondary) else if (function == EngineKeyFunctions.UseSecondary)
_verbs.VerbMenu.OpenVerbMenu(uid, true); _verb.OpenVerbMenu(uid, true);
else else
return; return;
@@ -153,7 +153,7 @@ public sealed class AdminUIController : UIController, IOnStateEntered<GameplaySt
if (function == EngineKeyFunctions.UIClick) if (function == EngineKeyFunctions.UIClick)
_conHost.ExecuteCommand($"vv {uid}"); _conHost.ExecuteCommand($"vv {uid}");
else if (function == EngineKeyFunctions.UseSecondary) else if (function == EngineKeyFunctions.UseSecondary)
_verbs.VerbMenu.OpenVerbMenu(uid, true); _verb.OpenVerbMenu(uid, true);
else else
return; return;

View File

@@ -3,10 +3,12 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Content.Client.CombatMode; using Content.Client.CombatMode;
using Content.Client.ContextMenu.UI; using Content.Client.ContextMenu.UI;
using Content.Client.Gameplay;
using Content.Shared.Input; using Content.Shared.Input;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Robust.Client.Player; using Robust.Client.Player;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Input; using Robust.Shared.Input;
using Robust.Shared.IoC; using Robust.Shared.IoC;
@@ -20,34 +22,53 @@ namespace Content.Client.Verbs.UI
/// This class handles the displaying of the verb menu. /// This class handles the displaying of the verb menu.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// In addition to the normal <see cref="ContextMenuPresenter"/> functionality, this also provides functions /// In addition to the normal <see cref="ContextMenuUIController"/> functionality, this also provides functions
/// open a verb menu for a given entity, add verbs to it, and add server-verbs when the server response is /// open a verb menu for a given entity, add verbs to it, and add server-verbs when the server response is
/// received. /// received.
/// </remarks> /// </remarks>
public sealed class VerbMenuPresenter : ContextMenuPresenter public sealed class VerbMenuUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>
{ {
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly ContextMenuUIController _context = default!;
private readonly CombatModeSystem _combatMode; [UISystemDependency] private readonly CombatModeSystem _combatMode = default!;
private readonly VerbSystem _verbSystem; [UISystemDependency] private readonly VerbSystem _verbSystem = default!;
public EntityUid CurrentTarget; public EntityUid CurrentTarget;
public SortedSet<Verb> CurrentVerbs = new(); public SortedSet<Verb> CurrentVerbs = new();
public VerbMenuPresenter(CombatModeSystem combatMode, VerbSystem verbSystem) /// <summary>
/// Separate from <see cref="ContextMenuUIController.RootMenu"/>, since we can open a verb menu as a submenu
/// of an entity menu element. If that happens, we need to be aware and close it properly.
/// </summary>
public ContextMenuPopup? OpenMenu = null;
public void OnStateEntered(GameplayState state)
{ {
IoCManager.InjectDependencies(this); _context.OnContextKeyEvent += OnKeyBindDown;
_combatMode = combatMode; _context.OnContextClosed += Close;
_verbSystem = verbSystem; _verbSystem.OnVerbsResponse += HandleVerbsResponse;
}
public void OnStateExited(GameplayState state)
{
_context.OnContextKeyEvent -= OnKeyBindDown;
_context.OnContextClosed -= Close;
if (_verbSystem != null)
_verbSystem.OnVerbsResponse -= HandleVerbsResponse;
Close();
} }
/// <summary> /// <summary>
/// Open a verb menu and fill it work verbs applicable to the given target entity. /// Open a verb menu and fill it with verbs applicable to the given target entity.
/// </summary> /// </summary>
/// <param name="target">Entity to get verbs on.</param> /// <param name="target">Entity to get verbs on.</param>
/// <param name="force">Used to force showing all verbs (mostly for admins).</param> /// <param name="force">Used to force showing all verbs (mostly for admins).</param>
public void OpenVerbMenu(EntityUid target, bool force = false) /// <param name="popup">
/// If this is not null, verbs will be placed into the given popup instead.
/// </param>
public void OpenVerbMenu(EntityUid target, bool force = false, ContextMenuPopup? popup=null)
{ {
if (_playerManager.LocalPlayer?.ControlledEntity is not {Valid: true} user || if (_playerManager.LocalPlayer?.ControlledEntity is not {Valid: true} user ||
_combatMode.IsInCombatMode(user)) _combatMode.IsInCombatMode(user))
@@ -55,53 +76,59 @@ namespace Content.Client.Verbs.UI
Close(); Close();
var menu = popup ?? _context.RootMenu;
menu.MenuBody.DisposeAllChildren();
CurrentTarget = target; CurrentTarget = target;
CurrentVerbs = _verbSystem.GetVerbs(target, user, Verb.VerbTypes, force); CurrentVerbs = _verbSystem.GetVerbs(target, user, Verb.VerbTypes, force);
OpenMenu = menu;
// Fill in client-side verbs. // Fill in client-side verbs.
FillVerbPopup(); FillVerbPopup(menu);
// Add indicator that some verbs may be missing. // Add indicator that some verbs may be missing.
// I long for the day when verbs will all be predicted and this becomes unnecessary. // I long for the day when verbs will all be predicted and this becomes unnecessary.
if (!target.IsClientSide()) if (!target.IsClientSide())
{ {
AddElement(RootMenu, new ContextMenuElement(Loc.GetString("verb-system-waiting-on-server-text"))); _context.AddElement(menu, new ContextMenuElement(Loc.GetString("verb-system-waiting-on-server-text")));
} }
// Show the menu // if popup isn't null (ie we are opening out of an entity menu element),
RootMenu.SetPositionLast(); // assume that that is going to handle opening the submenu properly
if (popup != null)
return;
// Show the menu at mouse pos
menu.SetPositionLast();
var box = UIBox2.FromDimensions(_userInterfaceManager.MousePositionScaled.Position, (1, 1)); var box = UIBox2.FromDimensions(_userInterfaceManager.MousePositionScaled.Position, (1, 1));
RootMenu.Open(box); menu.Open(box);
} }
/// <summary> /// <summary>
/// Fill the verb pop-up using the verbs stored in <see cref="CurrentVerbs"/> /// Fill the verb pop-up using the verbs stored in <see cref="CurrentVerbs"/>
/// </summary> /// </summary>
private void FillVerbPopup() private void FillVerbPopup(ContextMenuPopup popup)
{ {
if (RootMenu == null)
return;
HashSet<string> listedCategories = new(); HashSet<string> listedCategories = new();
foreach (var verb in CurrentVerbs) foreach (var verb in CurrentVerbs)
{ {
if (verb.Category == null) if (verb.Category == null)
{ {
var element = new VerbMenuElement(verb); var element = new VerbMenuElement(verb);
AddElement(RootMenu, element); _context.AddElement(popup, element);
} }
else if (listedCategories.Add(verb.Category.Text)) else if (listedCategories.Add(verb.Category.Text))
AddVerbCategory(verb.Category); AddVerbCategory(verb.Category, popup);
} }
RootMenu.InvalidateMeasure(); popup.InvalidateMeasure();
} }
/// <summary> /// <summary>
/// Add a verb category button to the pop-up /// Add a verb category button to the pop-up
/// </summary> /// </summary>
public void AddVerbCategory(VerbCategory category) public void AddVerbCategory(VerbCategory category, ContextMenuPopup popup)
{ {
// Get a list of the verbs in this category // Get a list of the verbs in this category
List<Verb> verbsInCategory = new(); List<Verb> verbsInCategory = new();
@@ -119,10 +146,10 @@ namespace Content.Client.Verbs.UI
return; return;
var element = new VerbMenuElement(category, verbsInCategory[0].TextStyleClass); var element = new VerbMenuElement(category, verbsInCategory[0].TextStyleClass);
AddElement(RootMenu, element); _context.AddElement(popup, element);
// Create the pop-up that appears when hovering over this element // Create the pop-up that appears when hovering over this element
element.SubMenu = new ContextMenuPopup(this, element); element.SubMenu = new ContextMenuPopup(_context, element);
foreach (var verb in verbsInCategory) foreach (var verb in verbsInCategory)
{ {
var subElement = new VerbMenuElement(verb) var subElement = new VerbMenuElement(verb)
@@ -130,7 +157,7 @@ namespace Content.Client.Verbs.UI
IconVisible = drawIcons, IconVisible = drawIcons,
TextVisible = !category.IconsOnly TextVisible = !category.IconsOnly
}; };
AddElement(element.SubMenu, subElement); _context.AddElement(element.SubMenu, subElement);
} }
element.SubMenu.MenuBody.Columns = category.Columns; element.SubMenu.MenuBody.Columns = category.Columns;
@@ -139,23 +166,23 @@ namespace Content.Client.Verbs.UI
/// <summary> /// <summary>
/// Add verbs from the server to <see cref="CurrentVerbs"/> and update the verb menu. /// Add verbs from the server to <see cref="CurrentVerbs"/> and update the verb menu.
/// </summary> /// </summary>
public void AddServerVerbs(List<Verb>? verbs) public void AddServerVerbs(List<Verb>? verbs, ContextMenuPopup popup)
{ {
RootMenu.MenuBody.DisposeAllChildren(); popup.MenuBody.DisposeAllChildren();
// Verbs may be null if the server does not think we can see the target entity. This **should** not happen. // Verbs may be null if the server does not think we can see the target entity. This **should** not happen.
if (verbs == null) if (verbs == null)
{ {
// remove "waiting for server..." and inform user that something went wrong. // remove "waiting for server..." and inform user that something went wrong.
AddElement(RootMenu, new ContextMenuElement(Loc.GetString("verb-system-null-server-response"))); _context.AddElement(popup, new ContextMenuElement(Loc.GetString("verb-system-null-server-response")));
return; return;
} }
CurrentVerbs.UnionWith(verbs); CurrentVerbs.UnionWith(verbs);
FillVerbPopup(); FillVerbPopup(popup);
} }
public override void OnKeyBindDown(ContextMenuElement element, GUIBoundKeyEventArgs args) public void OnKeyBindDown(ContextMenuElement element, GUIBoundKeyEventArgs args)
{ {
if (args.Function != EngineKeyFunctions.Use && args.Function != ContentKeyFunctions.ActivateItemInWorld) if (args.Function != EngineKeyFunctions.Use && args.Function != ContentKeyFunctions.ActivateItemInWorld)
return; return;
@@ -185,7 +212,7 @@ namespace Content.Client.Verbs.UI
if (verbElement.SubMenu.MenuBody.ChildCount != 1 if (verbElement.SubMenu.MenuBody.ChildCount != 1
|| verbElement.SubMenu.MenuBody.Children.First() is not VerbMenuElement verbMenuElement) || verbElement.SubMenu.MenuBody.Children.First() is not VerbMenuElement verbMenuElement)
{ {
OpenSubMenu(verbElement); _context.OpenSubMenu(verbElement);
return; return;
} }
@@ -200,11 +227,11 @@ namespace Content.Client.Verbs.UI
if (verbElement.SubMenu == null) if (verbElement.SubMenu == null)
{ {
var popupElement = new ConfirmationMenuElement(verb, "Confirm"); var popupElement = new ConfirmationMenuElement(verb, "Confirm");
verbElement.SubMenu = new ContextMenuPopup(this, verbElement); verbElement.SubMenu = new ContextMenuPopup(_context, verbElement);
AddElement(verbElement.SubMenu, popupElement); _context.AddElement(verbElement.SubMenu, popupElement);
} }
OpenSubMenu(verbElement); _context.OpenSubMenu(verbElement);
} }
else else
{ {
@@ -212,11 +239,28 @@ namespace Content.Client.Verbs.UI
} }
} }
private void Close()
{
if (OpenMenu == null)
return;
OpenMenu.Close();
OpenMenu = null;
}
private void HandleVerbsResponse(VerbsResponseEvent msg)
{
if (OpenMenu == null || !OpenMenu.Visible || CurrentTarget != msg.Entity)
return;
AddServerVerbs(msg.Verbs, OpenMenu);
}
private void ExecuteVerb(Verb verb) private void ExecuteVerb(Verb verb)
{ {
_verbSystem.ExecuteVerb(CurrentTarget, verb); _verbSystem.ExecuteVerb(CurrentTarget, verb);
if (verb.CloseMenu) if (verb.CloseMenu)
_verbSystem.CloseAllMenus(); _context.Close();
} }
} }
} }

View File

@@ -17,6 +17,7 @@ using Robust.Shared.Map;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Robust.Client.UserInterface;
namespace Content.Client.Verbs namespace Content.Client.Verbs
{ {
@@ -36,9 +37,6 @@ namespace Content.Client.Verbs
/// </summary> /// </summary>
public const float EntityMenuLookupSize = 0.25f; public const float EntityMenuLookupSize = 0.25f;
public EntityMenuPresenter EntityMenu = default!;
public VerbMenuPresenter VerbMenu = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!; [Dependency] private readonly IEyeManager _eyeManager = default!;
/// <summary> /// <summary>
@@ -46,41 +44,13 @@ namespace Content.Client.Verbs
/// </summary> /// </summary>
public MenuVisibility Visibility; public MenuVisibility Visibility;
public Action<VerbsResponseEvent>? OnVerbsResponse;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
UpdatesOutsidePrediction = true;
SubscribeNetworkEvent<RoundRestartCleanupEvent>(Reset);
SubscribeNetworkEvent<VerbsResponseEvent>(HandleVerbResponse); SubscribeNetworkEvent<VerbsResponseEvent>(HandleVerbResponse);
EntityMenu = new(this);
VerbMenu = new(_combatMode, this);
}
public void Reset(RoundRestartCleanupEvent ev)
{
CloseAllMenus();
}
public override void Shutdown()
{
base.Shutdown();
EntityMenu?.Dispose();
VerbMenu?.Dispose();
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
EntityMenu?.Update();
}
public void CloseAllMenus()
{
EntityMenu.Close();
VerbMenu.Close();
} }
/// <summary> /// <summary>
@@ -259,10 +229,7 @@ namespace Content.Client.Verbs
private void HandleVerbResponse(VerbsResponseEvent msg) private void HandleVerbResponse(VerbsResponseEvent msg)
{ {
if (!VerbMenu.RootMenu.Visible || VerbMenu.CurrentTarget != msg.Entity) OnVerbsResponse?.Invoke(msg);
return;
VerbMenu.AddServerVerbs(msg.Verbs);
} }
} }