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

@@ -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,12 +45,12 @@ namespace Content.Client.ContextMenu.UI
LayoutContainer.SetGrowVertical(CountLabel, LayoutContainer.GrowDirection.Begin);
Entity = entity;
if (Entity != null)
{
Count = 1;
CountLabel.Visible = false;
UpdateEntity();
}
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();
}
}
}
}