Context menu UI backend refactor & better UX (#13318)
closes https://github.com/space-wizards/space-station-14/issues/9209
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user