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.UserInterface.Controls;
using Content.Client.Verbs;
using Content.Client.Verbs.UI;
using Content.Shared.Administration;
using Content.Shared.Input;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Input;
@@ -56,7 +58,7 @@ namespace Content.Client.Administration.UI.CustomControls
}
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.Shared.CombatMode;
using Content.Shared.Targeting;
using Robust.Client.Player;
using Robust.Client.UserInterface;
namespace Content.Client.CombatMode
{
@@ -38,8 +40,7 @@ namespace Content.Client.CombatMode
return;
}
var verbs = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<VerbSystem>();
verbs.CloseAllMenus();
IoCManager.Resolve<IUserInterfaceManager>().GetUIController<ContextMenuUIController>().Close();
}
}
}

View File

@@ -12,7 +12,7 @@ namespace Content.Client.Commands
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)
{
if (args.Length != 1)
@@ -27,7 +27,7 @@ namespace Content.Client.Commands
return;
}
if (id < 0 ||id > EntityMenuPresenter.GroupingTypesCount - 1)
if (id < 0 ||id > EntityMenuUIController.GroupingTypesCount - 1)
{
shell.WriteLine($"{args[0]} is not a valid integer.");
return;

View File

@@ -31,14 +31,14 @@ namespace Content.Client.ContextMenu.UI
/// </summary>
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);
MenuPanel.SetOnlyStyleClass(StyleClassContextMenuPopup);
_presenter = presenter;
_uiController = uiController;
ParentElement = parentElement;
// 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;
UserInterfaceManager.ModalRoot.AddChild(this);
MenuBody.OnChildRemoved += ctrl => _presenter.OnRemoveElement(this, ctrl);
MenuBody.OnChildRemoved += ctrl => _uiController.OnRemoveElement(this, ctrl);
MenuBody.VSeparationOverride = 0;
MenuBody.HSeparationOverride = 0;
@@ -67,13 +67,13 @@ namespace Content.Client.ContextMenu.UI
OnPopupHide += () =>
{
if (ParentElement != null)
_presenter.CloseSubMenus(ParentElement.ParentMenu);
_uiController.CloseSubMenus(ParentElement.ParentMenu);
};
}
protected override void Dispose(bool disposing)
{
MenuBody.OnChildRemoved -= ctrl => _presenter.OnRemoveElement(this, ctrl);
MenuBody.OnChildRemoved -= ctrl => _uiController.OnRemoveElement(this, ctrl);
ParentElement = null;
base.Dispose(disposing);
}

View File

@@ -1,24 +1,26 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Content.Client.Gameplay;
using Robust.Client.UserInterface;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Client.UserInterface.Controllers;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Client.ContextMenu.UI
{
/// <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>
/// <remarks>
/// This largely involves setting up timers to open and close sub-menus when hovering over other menu elements.
/// </remarks>
[Virtual]
public class ContextMenuPresenter : IDisposable
public sealed class ContextMenuUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>
{
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();
/// <summary>
@@ -31,30 +33,35 @@ namespace Content.Client.ContextMenu.UI
/// </summary>
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.OnPopupHide += RootMenu.MenuBody.DisposeAllChildren;
RootMenu.OnPopupHide += Close;
Menus.Push(RootMenu);
}
/// <summary>
/// Dispose of all UI elements.
/// </summary>
public virtual void Dispose()
public void OnStateExited(GameplayState state)
{
RootMenu.OnPopupHide -= RootMenu.MenuBody.DisposeAllChildren;
Close();
RootMenu.OnPopupHide -= Close;
RootMenu.Dispose();
}
/// <summary>
/// Close and clear the root menu. This will also dispose any sub-menus.
/// </summary>
public virtual void Close()
public void Close()
{
RootMenu.Close();
RootMenu.MenuBody.DisposeAllChildren();
CancelOpen?.Cancel();
CancelClose?.Cancel();
OnContextClosed?.Invoke();
}
/// <summary>
@@ -82,7 +89,7 @@ namespace Content.Client.ContextMenu.UI
/// <summary>
/// Start a timer to open this element's sub-menu.
/// </summary>
public virtual void OnMouseEntered(ContextMenuElement element)
private void OnMouseEntered(ContextMenuElement element)
{
if (!Menus.TryPeek(out var topMenu))
{
@@ -100,6 +107,7 @@ namespace Content.Client.ContextMenu.UI
CancelOpen?.Cancel();
CancelOpen = new();
Timer.Spawn(HoverDelay, () => OpenSubMenu(element), CancelOpen.Token);
OnContextMouseEntered?.Invoke(element);
}
/// <summary>
@@ -108,7 +116,7 @@ namespace Content.Client.ContextMenu.UI
/// <remarks>
/// Note that this timer will be aborted when entering the actual sub-menu itself.
/// </remarks>
public virtual void OnMouseExited(ContextMenuElement element)
private void OnMouseExited(ContextMenuElement element)
{
CancelOpen?.Cancel();
@@ -118,9 +126,13 @@ namespace Content.Client.ContextMenu.UI
CancelClose?.Cancel();
CancelClose = new();
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>
/// Opens a new sub menu, and close the old one.
@@ -128,7 +140,7 @@ namespace Content.Client.ContextMenu.UI
/// <remarks>
/// If the given element has no sub-menu, just close the current one.
/// </remarks>
public virtual void OpenSubMenu(ContextMenuElement element)
public void OpenSubMenu(ContextMenuElement element)
{
if (!Menus.TryPeek(out var topMenu))
{
@@ -164,6 +176,7 @@ namespace Content.Client.ContextMenu.UI
element.SubMenu.SetPositionLast();
Menus.Push(element.SubMenu);
OnSubMenuOpened?.Invoke(element);
}
/// <summary>

View File

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

View File

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

View File

@@ -1,10 +1,9 @@
using System.Collections.Generic;
using System.Linq;
using Content.Client.CombatMode;
using Content.Client.Examine;
using Content.Client.Gameplay;
using Content.Client.Verbs;
using Content.Client.Viewport;
using Content.Client.Verbs.UI;
using Content.Shared.CCVar;
using Content.Shared.CombatMode;
using Content.Shared.Examine;
@@ -15,14 +14,11 @@ using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
namespace Content.Client.ContextMenu.UI
@@ -31,11 +27,11 @@ namespace Content.Client.ContextMenu.UI
/// This class handles the displaying of the entity context menu.
/// </summary>
/// <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
/// them from the menu as they move out of sight.
/// </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 IEntityManager _entityManager = default!;
@@ -46,11 +42,15 @@ namespace Content.Client.ContextMenu.UI
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly ContextMenuUIController _context = default!;
[Dependency] private readonly VerbMenuUIController _verb = default!;
private readonly VerbSystem _verbSystem;
private readonly ExamineSystem _examineSystem;
private readonly TransformSystem _xform;
private readonly SharedCombatModeSystem _combatMode;
[UISystemDependency] private readonly VerbSystem _verbSystem = default!;
[UISystemDependency] private readonly ExamineSystem _examineSystem = default!;
[UISystemDependency] private readonly TransformSystem _xform = default!;
[UISystemDependency] private readonly CombatModeSystem _combatMode = default!;
private bool _updating;
/// <summary>
/// This maps the currently displayed entities to the actual GUI elements.
@@ -60,27 +60,26 @@ namespace Content.Client.ContextMenu.UI
/// </remarks>
public Dictionary<EntityUid, EntityMenuElement> Elements = new();
public EntityMenuPresenter(VerbSystem verbSystem) : base()
public void OnStateEntered(GameplayState state)
{
IoCManager.InjectDependencies(this);
_verbSystem = verbSystem;
_examineSystem = _entityManager.EntitySysManager.GetEntitySystem<ExamineSystem>();
_combatMode = _entityManager.EntitySysManager.GetEntitySystem<CombatModeSystem>();
_xform = _entityManager.EntitySysManager.GetEntitySystem<TransformSystem>();
_updating = true;
_cfg.OnValueChanged(CCVars.EntityMenuGroupingType, OnGroupingChanged, true);
_context.OnContextMouseEntered += OnMouseEntered;
_context.OnContextKeyEvent += OnKeyBindDown;
CommandBinds.Builder
.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();
CommandBinds.Unregister<EntityMenuPresenter>();
_cfg.UnsubValueChanged(CCVars.EntityMenuGroupingType, OnGroupingChanged);
_context.OnContextMouseEntered -= OnMouseEntered;
_context.OnContextKeyEvent -= OnKeyBindDown;
CommandBinds.Unregister<EntityMenuUIController>();
}
/// <summary>
@@ -89,8 +88,8 @@ namespace Content.Client.ContextMenu.UI
public void OpenRootMenu(List<EntityUid> entities)
{
// close any old menus first.
if (RootMenu.Visible)
Close();
if (_context.RootMenu.Visible)
_context.Close();
var entitySpriteStates = GroupEntities(entities);
var orderedStates = entitySpriteStates.ToList();
@@ -99,12 +98,30 @@ namespace Content.Client.ContextMenu.UI
AddToUI(orderedStates);
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)
return;
@@ -116,14 +133,6 @@ namespace Content.Client.ContextMenu.UI
if (_entityManager.Deleted(entity))
return;
// open verb menu?
if (args.Function == EngineKeyFunctions.UseSecondary)
{
_verbSystem.VerbMenu.OpenVerbMenu(entity.Value);
args.Handle();
return;
}
// do examination?
if (args.Function == ContentKeyFunctions.ExamineEntity)
{
@@ -154,9 +163,8 @@ namespace Content.Client.ContextMenu.UI
inputSys.HandleInputCommand(session, func, message);
}
_verbSystem.CloseAllMenus();
_context.Close();
args.Handle();
return;
}
}
@@ -182,9 +190,12 @@ namespace Content.Client.ContextMenu.UI
/// <summary>
/// Check that entities in the context menu are still visible. If not, remove them from the context menu.
/// </summary>
public void Update()
public override void FrameUpdate(FrameEventArgs args)
{
if (!RootMenu.Visible)
if (!_updating || _context.RootMenu == null)
return;
if (!_context.RootMenu.Visible)
return;
if (_playerManager.LocalPlayer?.ControlledEntity is not { } player ||
@@ -229,7 +240,8 @@ namespace Content.Client.ContextMenu.UI
foreach (var entity in entityGroups[0])
{
var element = new EntityMenuElement(entity);
AddElement(RootMenu, element);
element.SubMenu = new ContextMenuPopup(_context, element);
_context.AddElement(_context.RootMenu, element);
Elements.TryAdd(entity, element);
}
return;
@@ -245,7 +257,8 @@ namespace Content.Client.ContextMenu.UI
// this group only has a single entity, add a simple menu element
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);
}
@@ -257,17 +270,18 @@ namespace Content.Client.ContextMenu.UI
private void AddGroupToUI(List<EntityUid> group)
{
EntityMenuElement element = new();
ContextMenuPopup subMenu = new(this, element);
ContextMenuPopup subMenu = new(_context, element);
foreach (var entity in group)
{
var subElement = new EntityMenuElement(entity);
AddElement(subMenu, subElement);
subElement.SubMenu = new ContextMenuPopup(_context, subElement);
_context.AddElement(subMenu, subElement);
Elements.TryAdd(entity, subElement);
}
UpdateElement(element);
AddElement(RootMenu, element);
_context.AddElement(_context.RootMenu, element);
}
/// <summary>
@@ -291,13 +305,9 @@ namespace Content.Client.ContextMenu.UI
if (parent is EntityMenuElement 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 (RootMenu.MenuBody.ChildCount == 0)
Close();
if (_context.RootMenu.MenuBody.ChildCount == 0)
_context.Close();
}
/// <summary>
@@ -376,17 +386,5 @@ namespace Content.Client.ContextMenu.UI
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.Timing;
using System.Diagnostics.CodeAnalysis;
using Content.Client.Verbs.UI;
using Robust.Client.UserInterface;
namespace Content.Client.Hands.Systems
{
@@ -22,11 +24,11 @@ namespace Content.Client.Hands.Systems
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IUserInterfaceManager _ui = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly StrippableSystem _stripSys = default!;
[Dependency] private readonly ExamineSystem _examine = default!;
[Dependency] private readonly VerbSystem _verbs = default!;
public event Action<string, HandLocation>? OnPlayerAddHand;
public event Action<string>? OnPlayerRemoveHand;
@@ -240,7 +242,7 @@ namespace Content.Client.Hands.Systems
return;
}
_verbs.VerbMenu.OpenVerbMenu(entity);
_ui.GetUIController<VerbMenuUIController>().OpenVerbMenu(entity);
}
public void UIHandAltActivateItem(string handName)

View File

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

View File

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

View File

@@ -3,10 +3,12 @@ using System.Collections.Generic;
using System.Linq;
using Content.Client.CombatMode;
using Content.Client.ContextMenu.UI;
using Content.Client.Gameplay;
using Content.Shared.Input;
using Content.Shared.Verbs;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.IoC;
@@ -20,34 +22,53 @@ namespace Content.Client.Verbs.UI
/// This class handles the displaying of the verb menu.
/// </summary>
/// <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
/// received.
/// </remarks>
public sealed class VerbMenuPresenter : ContextMenuPresenter
public sealed class VerbMenuUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly ContextMenuUIController _context = default!;
private readonly CombatModeSystem _combatMode;
private readonly VerbSystem _verbSystem;
[UISystemDependency] private readonly CombatModeSystem _combatMode = default!;
[UISystemDependency] private readonly VerbSystem _verbSystem = default!;
public EntityUid CurrentTarget;
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);
_combatMode = combatMode;
_verbSystem = verbSystem;
_context.OnContextKeyEvent += OnKeyBindDown;
_context.OnContextClosed += Close;
_verbSystem.OnVerbsResponse += HandleVerbsResponse;
}
public void OnStateExited(GameplayState state)
{
_context.OnContextKeyEvent -= OnKeyBindDown;
_context.OnContextClosed -= Close;
if (_verbSystem != null)
_verbSystem.OnVerbsResponse -= HandleVerbsResponse;
Close();
}
/// <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>
/// <param name="target">Entity to get verbs on.</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 ||
_combatMode.IsInCombatMode(user))
@@ -55,53 +76,59 @@ namespace Content.Client.Verbs.UI
Close();
var menu = popup ?? _context.RootMenu;
menu.MenuBody.DisposeAllChildren();
CurrentTarget = target;
CurrentVerbs = _verbSystem.GetVerbs(target, user, Verb.VerbTypes, force);
OpenMenu = menu;
// Fill in client-side verbs.
FillVerbPopup();
FillVerbPopup(menu);
// Add indicator that some verbs may be missing.
// I long for the day when verbs will all be predicted and this becomes unnecessary.
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
RootMenu.SetPositionLast();
// if popup isn't null (ie we are opening out of an entity menu element),
// 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));
RootMenu.Open(box);
menu.Open(box);
}
/// <summary>
/// Fill the verb pop-up using the verbs stored in <see cref="CurrentVerbs"/>
/// </summary>
private void FillVerbPopup()
private void FillVerbPopup(ContextMenuPopup popup)
{
if (RootMenu == null)
return;
HashSet<string> listedCategories = new();
foreach (var verb in CurrentVerbs)
{
if (verb.Category == null)
{
var element = new VerbMenuElement(verb);
AddElement(RootMenu, element);
_context.AddElement(popup, element);
}
else if (listedCategories.Add(verb.Category.Text))
AddVerbCategory(verb.Category);
AddVerbCategory(verb.Category, popup);
}
RootMenu.InvalidateMeasure();
popup.InvalidateMeasure();
}
/// <summary>
/// Add a verb category button to the pop-up
/// </summary>
public void AddVerbCategory(VerbCategory category)
public void AddVerbCategory(VerbCategory category, ContextMenuPopup popup)
{
// Get a list of the verbs in this category
List<Verb> verbsInCategory = new();
@@ -119,10 +146,10 @@ namespace Content.Client.Verbs.UI
return;
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
element.SubMenu = new ContextMenuPopup(this, element);
element.SubMenu = new ContextMenuPopup(_context, element);
foreach (var verb in verbsInCategory)
{
var subElement = new VerbMenuElement(verb)
@@ -130,7 +157,7 @@ namespace Content.Client.Verbs.UI
IconVisible = drawIcons,
TextVisible = !category.IconsOnly
};
AddElement(element.SubMenu, subElement);
_context.AddElement(element.SubMenu, subElement);
}
element.SubMenu.MenuBody.Columns = category.Columns;
@@ -139,23 +166,23 @@ namespace Content.Client.Verbs.UI
/// <summary>
/// Add verbs from the server to <see cref="CurrentVerbs"/> and update the verb menu.
/// </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.
if (verbs == null)
{
// 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;
}
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)
return;
@@ -185,7 +212,7 @@ namespace Content.Client.Verbs.UI
if (verbElement.SubMenu.MenuBody.ChildCount != 1
|| verbElement.SubMenu.MenuBody.Children.First() is not VerbMenuElement verbMenuElement)
{
OpenSubMenu(verbElement);
_context.OpenSubMenu(verbElement);
return;
}
@@ -200,11 +227,11 @@ namespace Content.Client.Verbs.UI
if (verbElement.SubMenu == null)
{
var popupElement = new ConfirmationMenuElement(verb, "Confirm");
verbElement.SubMenu = new ContextMenuPopup(this, verbElement);
AddElement(verbElement.SubMenu, popupElement);
verbElement.SubMenu = new ContextMenuPopup(_context, verbElement);
_context.AddElement(verbElement.SubMenu, popupElement);
}
OpenSubMenu(verbElement);
_context.OpenSubMenu(verbElement);
}
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)
{
_verbSystem.ExecuteVerb(CurrentTarget, verb);
if (verb.CloseMenu)
_verbSystem.CloseAllMenus();
_context.Close();
}
}
}

View File

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