Refactor Context Menus and make them use XAML & stylesheets (#4768)

* XAML verb menu

* fix ghost FOV

* spacing

* rename missed "ContextMenu"->"EntityMenu" instances

* move visibility checks to verb system

* update comment

* Remove CanSeeContainerCheck

* use ScrollContainer measure option

* MaxWidth / texxt line wrapping

* verb category default

Now when you click on a verb category, it should default to running the first member of that category.

This makes it much more convenient to eject/insert when there is only a single option

* only apply style to first verb category entry

* Use new visibility flags

* FoV -> Fov

* Revert "only apply style to first verb category entry"

This reverts commit 9a6a17dba600e3ae0421caed59fcab145c260c99.

* make all entity menu visibility checks clientside

* Fix empty unbuckle category

* fix merge
This commit is contained in:
Leon Friedrich
2021-10-28 18:21:19 +13:00
committed by GitHub
parent 224952110e
commit 49296e33a0
36 changed files with 1421 additions and 1535 deletions

View File

@@ -1,249 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Client.Interactable.Components;
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using static Robust.Client.UserInterface.Controls.BoxContainer;
using Vector2 = Robust.Shared.Maths.Vector2;
namespace Content.Client.ContextMenu.UI
{
public abstract class ContextMenuElement : Control
{
private static readonly Color HoverColor = Color.DarkSlateGray;
protected internal readonly ContextMenuPopup? ParentMenu;
protected ContextMenuElement(ContextMenuPopup? parentMenu)
{
ParentMenu = parentMenu;
MouseFilter = MouseFilterMode.Stop;
}
protected override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
if (UserInterfaceManager.CurrentlyHovered == this)
{
handle.DrawRect(PixelSizeBox, HoverColor);
}
}
}
public sealed class SingleContextElement : ContextMenuElement
{
public event Action? OnMouseHovering;
public event Action? OnExitedTree;
public IEntity ContextEntity{ get; }
public readonly StackContextElement? Pre;
public ISpriteComponent? SpriteComp { get; }
public InteractionOutlineComponent? OutlineComponent { get; }
public int OriginalDrawDepth { get; }
public bool DrawOutline { get; set; }
public SingleContextElement(IEntity entity, StackContextElement? pre, ContextMenuPopup? parentMenu) : base(parentMenu)
{
Pre = pre;
ContextEntity = entity;
if (ContextEntity.TryGetComponent(out ISpriteComponent? sprite))
{
SpriteComp = sprite;
OriginalDrawDepth = SpriteComp.DrawDepth;
}
OutlineComponent = ContextEntity.GetComponentOrNull<InteractionOutlineComponent>();
AddChild(
new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
Children =
{
new LayoutContainer
{
Children = { new SpriteView { Sprite = SpriteComp } }
},
new Label
{
Text = Loc.GetString(UserInterfaceManager.DebugMonitors.Visible ? $"{ContextEntity.Name} ({ContextEntity.Uid})" : ContextEntity.Name)
}
}, Margin = new Thickness(0,0,10,0)
}
);
}
protected override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
if (UserInterfaceManager.CurrentlyHovered == this)
{
OnMouseHovering?.Invoke();
}
}
protected override void ExitedTree()
{
OnExitedTree?.Invoke();
base.ExitedTree();
}
}
public sealed class StackContextElement : ContextMenuElement
{
public event Action? OnExitedTree;
public HashSet<IEntity> ContextEntities { get; }
public readonly StackContextElement? Pre;
private readonly SpriteView _spriteView;
private readonly Label _label;
public int EntitiesCount => ContextEntities.Count;
public StackContextElement(IEnumerable<IEntity> entities, StackContextElement? pre, ContextMenuPopup? parentMenu)
: base(parentMenu)
{
Pre = pre;
ContextEntities = new(entities);
_spriteView = new SpriteView
{
Sprite = ContextEntities.First().GetComponent<ISpriteComponent>()
};
_label = new Label
{
Text = Loc.GetString(ContextEntities.Count.ToString()),
StyleClasses = { StyleNano.StyleClassContextMenuCount }
};
LayoutContainer.SetAnchorPreset(_label, LayoutContainer.LayoutPreset.BottomRight);
LayoutContainer.SetGrowHorizontal(_label, LayoutContainer.GrowDirection.Begin);
LayoutContainer.SetGrowVertical(_label, LayoutContainer.GrowDirection.Begin);
AddChild(
new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
SeparationOverride = 6,
Children =
{
new LayoutContainer { Children = { _spriteView, _label } },
new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
SeparationOverride = 6,
Children =
{
new Label
{
Text = Loc.GetString(ContextEntities.First().Name)
},
new TextureRect
{
Texture = IoCManager.Resolve<IResourceCache>().GetTexture("/Textures/Interface/VerbIcons/group.svg.192dpi.png"),
TextureScale = (0.5f, 0.5f),
Stretch = TextureRect.StretchMode.KeepCentered,
}
}
}
}, Margin = new Thickness(0,0,10,0)
}
);
}
protected override void ExitedTree()
{
OnExitedTree?.Invoke();
base.ExitedTree();
}
public void RemoveEntity(IEntity entity)
{
ContextEntities.Remove(entity);
_label.Text = Loc.GetString(ContextEntities.Count.ToString());
_spriteView.Sprite = ContextEntities.FirstOrDefault(e => !e.Deleted)?.GetComponent<ISpriteComponent>();
}
}
public class ContextMenuPopup : Popup
{
public static readonly Color ButtonColor = Color.FromHex("#1119");
public static readonly Color BackgroundColor = Color.FromHex("#333E");
public const int MaxItemsBeforeScroll = 10;
public const int MarginSize = 2;
public const int ButtonHeight = 32;
public BoxContainer List { get; }
public ScrollContainer Scroll { get; }
public int Depth { get; }
public ContextMenuPopup(int depth = 0)
{
MaxHeight = MaxItemsBeforeScroll * (ButtonHeight + 2*MarginSize);
Depth = depth;
List = new() { Orientation = LayoutOrientation.Vertical };
Scroll = new()
{
HScrollEnabled = false,
Children = { List }
};
AddChild(new PanelContainer
{
Children = { Scroll },
PanelOverride = new StyleBoxFlat { BackgroundColor = BackgroundColor }
});
}
public void AddToMenu(Control element)
{
List.AddChild(new PanelContainer
{
Children = { element },
Margin = new Thickness(MarginSize, MarginSize, MarginSize, MarginSize),
PanelOverride = new StyleBoxFlat { BackgroundColor = ButtonColor }
});
}
public void RemoveFromMenu(ContextMenuElement element)
{
if (element.Parent != null)
{
List.RemoveChild(element.Parent);
InvalidateMeasure();
}
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
if (List.ChildCount == 0)
{
return Vector2.Zero;
}
Scroll.Measure(availableSize);
var size = List.DesiredSize;
// account for scroll bar width
if (size.Y > MaxHeight)
{
// Scroll._vScrollBar is private and ScrollContainer gives no size information :/
// 10 = Scroll._vScrollBar.DesiredSize
size.X += 10;
}
return size;
}
}
}

View File

@@ -0,0 +1,26 @@
<ContainerButton
xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:Content.Client.ContextMenu.UI"
MinHeight="{x:Static ui:ContextMenuElement.ElementHeight}">
<BoxContainer Orientation="Horizontal">
<Control
Name="Icon"
SetWidth="{x:Static ui:ContextMenuElement.ElementHeight}"
SetHeight="{x:Static ui:ContextMenuElement.ElementHeight}"/>
<RichTextLabel
Name="Label"
MaxWidth="300"
HorizontalExpand="True"
VerticalAlignment="Center"
Margin ="4 0 4 0"/>
<TextureRect
Name="ExpansionIndicator"
HorizontalAlignment="Right"
Stretch="KeepCentered"
TextureScale="0.5 0.5"
SetWidth="{x:Static ui:ContextMenuElement.ElementHeight}"
SetHeight="{x:Static ui:ContextMenuElement.ElementHeight}"
Visible ="false"/>
</BoxContainer>
</ContainerButton>

View File

@@ -0,0 +1,101 @@
using Content.Client.Resources;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Content.Client.ContextMenu.UI
{
/// <summary>
/// This is a basic entry in a context menu. It has a label and room for some sort of icon on the left.
/// If this entry has a sub-menu, it also shows a little ">" icon on the right.
/// </summary>
[GenerateTypedNameReferences]
public partial class ContextMenuElement : ContainerButton
{
public const string StyleClassContextMenuButton = "contextMenuButton";
public const float ElementMargin = 2;
public const float ElementHeight = 32;
/// <summary>
/// The menu that contains this element
/// </summary>
public ContextMenuPopup? ParentMenu;
private ContextMenuPopup? _subMenu;
/// <summary>
/// The pop-up menu that is opened when hovering over this element.
/// </summary>
public ContextMenuPopup? SubMenu
{
get => _subMenu;
set
{
_subMenu = value;
ExpansionIndicator.Visible = _subMenu != null;
}
}
/// <summary>
/// Convenience property to set label text.
/// </summary>
public string Text { set => Label.SetMessage(FormattedMessage.FromMarkupPermissive(value.Trim())); }
public ContextMenuElement(string? text = null)
{
RobustXamlLoader.Load(this);
Margin = new Thickness(ElementMargin, ElementMargin, ElementMargin, ElementMargin);
SetOnlyStyleClass(StyleClassContextMenuButton);
if (text != null)
Text = text;
ExpansionIndicator.Texture = IoCManager.Resolve<IResourceCache>()
.GetTexture("/Textures/Interface/VerbIcons/group.svg.192dpi.png");
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_subMenu?.Dispose();
_subMenu = null;
ParentMenu = null;
}
protected override void Draw(DrawingHandleScreen handle)
{
UpdateStyle();
base.Draw(handle);
}
/// <summary>
/// If this element's sub-menu is currently visible, give it the hovered pseudo class.
/// </summary>
/// <remarks>
/// Basically: if we are in a sub menu, keep the element in the parent menu highlighted even though we are
/// not actually hovering over it.
/// </remarks>
protected virtual void UpdateStyle()
{
if ((_subMenu?.Visible ?? false) && !HasStylePseudoClass(StylePseudoClassHover))
{
AddStylePseudoClass(StylePseudoClassHover);
return;
}
if (DrawMode == DrawModeEnum.Hover)
return;
if (_subMenu?.Visible ?? true)
return;
RemoveStylePseudoClass(StylePseudoClassHover);
}
}
}

View File

@@ -0,0 +1,6 @@
<Popup xmlns="https://spacestation14.io">
<PanelContainer Name="MenuPanel">
<ScrollContainer Name="Scroll" VerticalExpand="True" HScrollEnabled="False" ReturnMeasure="True">
</ScrollContainer>
</PanelContainer>
</Popup>

View File

@@ -0,0 +1,70 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.ContextMenu.UI
{
/// <summary>
/// The base context-menu pop-up window used for both the entity and verb menus.
/// </summary>
[GenerateTypedNameReferences]
public partial class ContextMenuPopup : Popup
{
public const string StyleClassContextMenuPopup = "contextMenuPopup";
/// <summary>
/// How many items to list before limiting the size and adding a scroll bar.
/// </summary>
public const int MaxItemsBeforeScroll = 10;
/// <summary>
/// If this pop-up is created by hovering over some element in another pop-up, this is that element.
/// </summary>
public ContextMenuElement? ParentElement;
/// <summary>
/// This is the main body of the menu. The menu entries should be added to this object.
/// </summary>
public BoxContainer MenuBody = new() { Orientation = LayoutOrientation.Vertical };
private ContextMenuPresenter _presenter;
public ContextMenuPopup (ContextMenuPresenter presenter, ContextMenuElement? parentElement) : base()
{
RobustXamlLoader.Load(this);
MenuPanel.SetOnlyStyleClass(StyleClassContextMenuPopup);
_presenter = presenter;
ParentElement = parentElement;
//XAML controls are private. So defining and adding MenuBody here instead.
Scroll.AddChild(MenuBody);
// Set Max Height based on MaxItemsBeforeScroll and the panel's style box
MenuPanel.ForceRunStyleUpdate();
MenuPanel.TryGetStyleProperty<StyleBox>(PanelContainer.StylePropertyPanel, out var box);
var styleSize = (box?.MinimumSize ?? Vector2.Zero) / UIScale;
MenuPanel.MaxHeight = MaxItemsBeforeScroll * (ContextMenuElement.ElementHeight + 2 * ContextMenuElement.ElementMargin) + styleSize.Y;
UserInterfaceManager.ModalRoot.AddChild(this);
MenuBody.OnChildRemoved += ctrl => _presenter.OnRemoveElement(this, ctrl);
if (ParentElement != null)
{
DebugTools.Assert(ParentElement.SubMenu == null);
ParentElement.SubMenu = this;
}
}
protected override void Dispose(bool disposing)
{
MenuBody.OnChildRemoved -= ctrl => _presenter.OnRemoveElement(this, ctrl);
ParentElement = null;
base.Dispose(disposing);
}
}
}

View File

@@ -1,389 +1,179 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading;
using Content.Client.Examine;
using Content.Client.Interactable;
using Content.Client.Items.Managers;
using Content.Client.Verbs;
using Content.Client.Viewport;
using Content.Shared.CCVar;
using Content.Shared.Input;
using Content.Shared.Interaction.Helpers;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Shared.Configuration;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Timing;
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
using Robust.Shared.Maths;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Client.ContextMenu.UI
{
/// <summary>
/// This class handles all the logic associated with showing a context menu.
/// </summary>
/// <remarks>
/// This largely involves setting up timers to open and close sub-menus when hovering over other menu elements.
/// </remarks>
public class ContextMenuPresenter : IDisposable
{
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
[Dependency] private readonly IItemSlotManager _itemSlotManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
public static readonly TimeSpan HoverDelay = TimeSpan.FromSeconds(0.2);
private CancellationTokenSource? _cancelHover;
private readonly IContextMenuView _contextMenuView;
private readonly VerbSystem _verbSystem;
public ContextMenuPopup RootMenu;
public Stack<ContextMenuPopup> Menus { get; } = new();
private MapCoordinates _mapCoordinates;
/// <summary>
/// Used to cancel the timer that opens menus.
/// </summary>
public CancellationTokenSource? CancelOpen;
public ContextMenuPresenter(VerbSystem verbSystem)
/// <summary>
/// Used to cancel the timer that closes menus.
/// </summary>
public CancellationTokenSource? CancelClose;
public ContextMenuPresenter()
{
IoCManager.InjectDependencies(this);
_verbSystem = verbSystem;
_contextMenuView = new ContextMenuView();
_contextMenuView.OnKeyBindDownSingle += OnKeyBindDownSingle;
_contextMenuView.OnMouseEnteredSingle += OnMouseEnteredSingle;
_contextMenuView.OnMouseExitedSingle += OnMouseExitedSingle;
_contextMenuView.OnMouseHoveringSingle += OnMouseHoveringSingle;
_contextMenuView.OnKeyBindDownStack += OnKeyBindDownStack;
_contextMenuView.OnMouseEnteredStack += OnMouseEnteredStack;
_contextMenuView.OnExitedTree += OnExitedTree;
_contextMenuView.OnCloseRootMenu += OnCloseRootMenu;
_contextMenuView.OnCloseChildMenu += OnCloseChildMenu;
_cfg.OnValueChanged(CCVars.ContextMenuGroupingType, _contextMenuView.OnGroupingContextMenuChanged, true);
CommandBinds.Builder
.Bind(ContentKeyFunctions.OpenContextMenu, new PointerInputCmdHandler(HandleOpenContextMenu))
.Register<ContextMenuPresenter>();
}
#region View Events
private void OnCloseChildMenu(object? sender, int depth)
{
_contextMenuView.CloseContextPopups(depth);
}
private void OnCloseRootMenu(object? sender, EventArgs e)
{
_contextMenuView.CloseContextPopups();
}
private void OnExitedTree(object? sender, ContextMenuElement e)
{
_contextMenuView.UpdateParents(e);
}
private void OnMouseEnteredStack(object? sender, StackContextElement e)
{
var realGlobalPosition = e.GlobalPosition;
_cancelHover?.Cancel();
_cancelHover = new();
Timer.Spawn(HoverDelay, () =>
{
if (_contextMenuView.Menus.Count == 0)
{
return;
}
OnCloseChildMenu(sender, e.ParentMenu?.Depth ?? 0);
var filteredEntities = e.ContextEntities.Where(entity => !entity.Deleted);
if (filteredEntities.Any())
{
_contextMenuView.AddChildMenu(filteredEntities, realGlobalPosition, e);
}
}, _cancelHover.Token);
}
private void OnKeyBindDownStack(object? sender, (GUIBoundKeyEventArgs, StackContextElement) e)
{
var (args, stack) = e;
var firstEntity = stack.ContextEntities.FirstOrDefault(ent => !ent.Deleted);
if (firstEntity == null) return;
if (args.Function == EngineKeyFunctions.Use || args.Function == ContentKeyFunctions.AltActivateItemInWorld || args.Function == ContentKeyFunctions.TryPullObject || args.Function == ContentKeyFunctions.MovePulledObject)
{
var inputSys = _systemManager.GetEntitySystem<InputSystem>();
var func = args.Function;
var funcId = _inputManager.NetworkBindMap.KeyFunctionID(func);
var message = new FullInputCmdMessage(_gameTiming.CurTick, _gameTiming.TickFraction, funcId,
BoundKeyState.Down, firstEntity.Transform.Coordinates, args.PointerLocation, firstEntity.Uid);
var session = _playerManager.LocalPlayer?.Session;
if (session != null)
{
inputSys.HandleInputCommand(session, func, message);
}
CloseAllMenus();
args.Handle();
return;
}
if (_itemSlotManager.OnButtonPressed(args, firstEntity))
{
CloseAllMenus();
}
}
private void OnMouseHoveringSingle(object? sender, SingleContextElement e)
{
if (!e.DrawOutline) return;
var localPlayer = _playerManager.LocalPlayer;
if (localPlayer?.ControlledEntity != null)
{
var inRange =
localPlayer.InRangeUnobstructed(e.ContextEntity, ignoreInsideBlocker: true);
// BUG: This assumes that the main viewport is the viewport that the context menu is active on.
// This is not necessarily true but we currently have no way to find the viewport (reliably)
// from the input event.
//
// This might be particularly important in the future with a more advanced mapping mode.
var renderScale = _eyeManager.MainViewport.GetRenderScale();
e.OutlineComponent?.UpdateInRange(inRange, renderScale);
}
}
private void OnMouseEnteredSingle(object? sender, SingleContextElement e)
{
// close other pop-ups after a short delay
_cancelHover?.Cancel();
_cancelHover = new();
Timer.Spawn(HoverDelay, () =>
{
if (_contextMenuView.Menus.Count == 0)
{
return;
}
OnCloseChildMenu(sender, e.ParentMenu?.Depth ?? 0);
}, _cancelHover.Token);
var entity = e.ContextEntity;
OnCloseChildMenu(sender, e.ParentMenu?.Depth ?? 0);
if (entity.Deleted) return;
var localPlayer = _playerManager.LocalPlayer;
if (localPlayer?.ControlledEntity == null) return;
var renderScale = _eyeManager.MainViewport.GetRenderScale();
e.OutlineComponent?.OnMouseEnter(localPlayer.InRangeUnobstructed(entity, ignoreInsideBlocker: true), renderScale);
if (e.SpriteComp != null)
{
e.SpriteComp.DrawDepth = (int) DrawDepth.HighlightedItems;
}
e.DrawOutline = true;
}
private void OnMouseExitedSingle(object? sender, SingleContextElement e)
{
if (!e.ContextEntity.Deleted)
{
if (e.SpriteComp != null)
{
e.SpriteComp.DrawDepth = e.OriginalDrawDepth;
}
e.OutlineComponent?.OnMouseLeave();
}
e.DrawOutline = false;
}
private void OnKeyBindDownSingle(object? sender, (GUIBoundKeyEventArgs, SingleContextElement) valueTuple)
{
var (args, single) = valueTuple;
var entity = single.ContextEntity;
if (args.Function == ContentKeyFunctions.OpenContextMenu)
{
_verbSystem.OnContextButtonPressed(entity);
args.Handle();
return;
}
if (args.Function == ContentKeyFunctions.ExamineEntity)
{
_systemManager.GetEntitySystem<ExamineSystem>().DoExamine(entity);
args.Handle();
return;
}
if (args.Function == EngineKeyFunctions.Use || args.Function == ContentKeyFunctions.AltActivateItemInWorld || args.Function == ContentKeyFunctions.Point ||
args.Function == ContentKeyFunctions.TryPullObject || args.Function == ContentKeyFunctions.MovePulledObject)
{
var inputSys = _systemManager.GetEntitySystem<InputSystem>();
var func = args.Function;
var funcId = _inputManager.NetworkBindMap.KeyFunctionID(func);
var message = new FullInputCmdMessage(_gameTiming.CurTick, _gameTiming.TickFraction, funcId,
BoundKeyState.Down, entity.Transform.Coordinates, args.PointerLocation, entity.Uid);
var session = _playerManager.LocalPlayer?.Session;
if (session != null)
{
inputSys.HandleInputCommand(session, func, message);
}
CloseAllMenus();
args.Handle();
return;
}
if (_itemSlotManager.OnButtonPressed(args, single.ContextEntity))
{
CloseAllMenus();
}
}
#endregion
#region Model Updates
private bool HandleOpenContextMenu(in PointerInputCmdHandler.PointerInputCmdArgs args)
{
if (args.State != BoundKeyState.Down)
{
return false;
}
if (_stateManager.CurrentState is not GameScreenBase)
{
return false;
}
var player = _playerManager.LocalPlayer?.ControlledEntity;
if (player == null)
{
return false;
}
_mapCoordinates = args.Coordinates.ToMap(_entityManager);
if (!_verbSystem.TryGetContextEntities(player, _mapCoordinates, out var entities, ignoreVisibility: _verbSystem.CanSeeAllContext))
return false;
// do we need to do visiblity checks?
if (_verbSystem.CanSeeAllContext)
{
_contextMenuView.AddRootMenu(entities);
return true;
}
//visibility checks
player.TryGetContainer(out var playerContainer);
foreach (var entity in entities.ToList())
{
if (!entity.TryGetComponent(out ISpriteComponent? spriteComponent) ||
!spriteComponent.Visible ||
!CanSeeContainerCheck(entity, playerContainer))
{
entities.Remove(entity);
}
}
if (entities.Count == 0)
return false;
_contextMenuView.AddRootMenu(entities);
return true;
RootMenu = new(this, null);
RootMenu.OnPopupHide += RootMenu.MenuBody.DisposeAllChildren;
Menus.Push(RootMenu);
}
/// <summary>
/// Can the player see the entity through any entity containers?
/// Dispose of all UI elements.
/// </summary>
public virtual void Dispose()
{
RootMenu.OnPopupHide -= RootMenu.MenuBody.DisposeAllChildren;
RootMenu.Dispose();
}
/// <summary>
/// Close and clear the root menu. This will also dispose any sub-menus.
/// </summary>
public virtual void Close()
{
RootMenu.Close();
CancelOpen?.Cancel();
CancelClose?.Cancel();
}
/// <summary>
/// Starts closing menus until the top-most menu is the given one.
/// </summary>
/// <remarks>
/// This is similar to <see cref="ContainerHelpers.IsInSameOrParentContainer()"/>, except that we do not
/// allow the player to be the "parent" container and we allow for see-through containers (display cases).
/// Note that this does not actually check if the given menu IS a sub menu of this presenter. In that case
/// this will close all menus.
/// </remarks>
private bool CanSeeContainerCheck(IEntity entity, IContainer? playerContainer)
public void CloseSubMenus(ContextMenuPopup? menu)
{
// is the player inside this entity?
if (playerContainer?.Owner == entity)
return true;
if (menu == null || !menu.Visible)
return;
entity.TryGetContainer(out var entityContainer);
// are they in the same container (or none?)
if (playerContainer == entityContainer)
return true;
// Is the entity in a display case?
if (playerContainer == null && entityContainer!.ShowContents)
return true;
return false;
while (Menus.TryPeek(out var subMenu) && subMenu != menu)
{
Menus.Pop().Close();
}
}
/// <summary>
/// Check that entities in the context menu are still visible. If not, remove them from the context menu.
/// Start a timer to open this element's sub-menu.
/// </summary>
public void Update()
public virtual void OnMouseEntered(ContextMenuElement element)
{
if (_contextMenuView.Elements.Count == 0)
var topMenu = Menus.Peek();
if (element.ParentMenu == topMenu || element.SubMenu == topMenu)
CancelClose?.Cancel();
if (element.SubMenu == topMenu)
return;
var player = _playerManager.LocalPlayer?.ControlledEntity;
// open the sub-menu after a short delay.
CancelOpen?.Cancel();
CancelOpen = new();
Timer.Spawn(HoverDelay, () => OpenSubMenu(element), CancelOpen.Token);
}
if (player == null)
/// <summary>
/// Start a timer to close this element's sub-menu.
/// </summary>
/// <remarks>
/// Note that this timer will be aborted when entering the actual sub-menu itself.
/// </remarks>
public virtual void OnMouseExited(ContextMenuElement element)
{
CancelOpen?.Cancel();
if (element.SubMenu == null)
return;
foreach (var entity in _contextMenuView.Elements.Keys.ToList())
{
if (entity.Deleted || !_verbSystem.CanSeeAllContext && !player.InRangeUnOccluded(entity))
{
_contextMenuView.RemoveEntity(entity);
if (_verbSystem.CurrentTarget == entity.Uid)
_verbSystem.CloseVerbMenu();
}
}
}
#endregion
public void CloseAllMenus()
{
_contextMenuView.CloseContextPopups();
_verbSystem.CloseVerbMenu();
CancelClose?.Cancel();
CancelClose = new();
Timer.Spawn(HoverDelay, () => CloseSubMenus(element.ParentMenu), CancelClose.Token);
}
public void Dispose()
public virtual void OnKeyBindDown(ContextMenuElement element, GUIBoundKeyEventArgs args) { }
/// <summary>
/// Opens a new sub menu, and close the old one.
/// </summary>
/// <remarks>
/// If the given element has no sub-menu, just close the current one.
/// </remarks>
public virtual void OpenSubMenu(ContextMenuElement element)
{
_contextMenuView.OnKeyBindDownSingle -= OnKeyBindDownSingle;
_contextMenuView.OnMouseEnteredSingle -= OnMouseEnteredSingle;
_contextMenuView.OnMouseExitedSingle -= OnMouseExitedSingle;
_contextMenuView.OnMouseHoveringSingle -= OnMouseHoveringSingle;
// If This is already the top most menu, do nothing.
if (element.SubMenu == Menus.Peek())
return;
_contextMenuView.OnKeyBindDownStack -= OnKeyBindDownStack;
_contextMenuView.OnMouseEnteredStack -= OnMouseEnteredStack;
// Was the parent menu closed or disposed before an open timer completed?
if (element.Disposed || element.ParentMenu == null || !element.ParentMenu.Visible)
return;
_contextMenuView.OnExitedTree -= OnExitedTree;
_contextMenuView.OnCloseRootMenu -= OnCloseRootMenu;
_contextMenuView.OnCloseChildMenu -= OnCloseChildMenu;
// Close any currently open sub-menus up to this element's parent menu.
CloseSubMenus(element.ParentMenu);
CommandBinds.Unregister<ContextMenuPresenter>();
if (element.SubMenu == null)
return;
// open pop-up adjacent to the parent element. We want the sub-menu elements to align with this element
// which depends on the panel container style margins.
var altPos = element.GlobalPosition;
var pos = altPos + (element.Width + 2*ContextMenuElement.ElementMargin, - 2*ContextMenuElement.ElementMargin);
element.SubMenu.Open(UIBox2.FromDimensions(pos, (1, 1)), altPos);
element.SubMenu.Close();
element.SubMenu.Open(UIBox2.FromDimensions(pos, (1, 1)), altPos);
// draw on top of other menus
element.SubMenu.SetPositionLast();
Menus.Push(element.SubMenu);
}
/// <summary>
/// Add an element to a menu and subscribe to GUI events.
/// </summary>
public void AddElement(ContextMenuPopup menu, ContextMenuElement element)
{
element.OnMouseEntered += _ => OnMouseEntered(element);
element.OnMouseExited += _ => OnMouseExited(element);
element.OnKeyBindDown += args => OnKeyBindDown(element, args);
element.ParentMenu = menu;
menu.MenuBody.AddChild(element);
menu.InvalidateMeasure();
}
/// <summary>
/// Removes event subscriptions when an element is removed from a menu,
/// </summary>
public void OnRemoveElement(ContextMenuPopup menu, Control control)
{
if (control is not ContextMenuElement element)
return;
element.OnMouseEntered -= _ => OnMouseEntered(element);
element.OnMouseExited -= _ => OnMouseExited(element);
element.OnKeyBindDown -= args => OnKeyBindDown(element, args);
menu.InvalidateMeasure();
}
}
}

View File

@@ -1,263 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Client.UserInterface;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
namespace Content.Client.ContextMenu.UI
{
public interface IContextMenuView : IDisposable
{
Dictionary<IEntity, ContextMenuElement> Elements { get; set; }
Stack<ContextMenuPopup> Menus { get; }
event EventHandler<(GUIBoundKeyEventArgs, SingleContextElement)>? OnKeyBindDownSingle;
event EventHandler<SingleContextElement>? OnMouseEnteredSingle;
event EventHandler<SingleContextElement>? OnMouseExitedSingle;
event EventHandler<SingleContextElement>? OnMouseHoveringSingle;
event EventHandler<(GUIBoundKeyEventArgs, StackContextElement)>? OnKeyBindDownStack;
event EventHandler<StackContextElement>? OnMouseEnteredStack;
event EventHandler<ContextMenuElement>? OnExitedTree;
event EventHandler? OnCloseRootMenu;
event EventHandler<int>? OnCloseChildMenu;
void UpdateParents(ContextMenuElement element);
void RemoveEntity(IEntity element);
void AddRootMenu(List<IEntity> entities);
void AddChildMenu(IEnumerable<IEntity> entities, Vector2 position, StackContextElement? stack);
void CloseContextPopups(int depth);
void CloseContextPopups();
void OnGroupingContextMenuChanged(int obj);
}
public partial class ContextMenuView : IContextMenuView
{
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
public Stack<ContextMenuPopup> Menus { get; }
public Dictionary<IEntity, ContextMenuElement> Elements { get; set; }
public event EventHandler<(GUIBoundKeyEventArgs, SingleContextElement)>? OnKeyBindDownSingle;
public event EventHandler<SingleContextElement>? OnMouseEnteredSingle;
public event EventHandler<SingleContextElement>? OnMouseExitedSingle;
public event EventHandler<SingleContextElement>? OnMouseHoveringSingle;
public event EventHandler<(GUIBoundKeyEventArgs, StackContextElement)>? OnKeyBindDownStack;
public event EventHandler<StackContextElement>? OnMouseEnteredStack;
public event EventHandler<ContextMenuElement>? OnExitedTree;
public event EventHandler? OnCloseRootMenu;
public event EventHandler<int>? OnCloseChildMenu;
public ContextMenuView()
{
IoCManager.InjectDependencies(this);
Menus = new Stack<ContextMenuPopup>();
Elements = new Dictionary<IEntity, ContextMenuElement>();
}
public void AddRootMenu(List<IEntity> entities)
{
Elements = new Dictionary<IEntity, ContextMenuElement>(entities.Count);
var rootContextMenu = new ContextMenuPopup();
rootContextMenu.OnPopupHide += () => OnCloseRootMenu?.Invoke(this, EventArgs.Empty);
Menus.Push(rootContextMenu);
var entitySpriteStates = GroupEntities(entities);
var orderedStates = entitySpriteStates.ToList();
orderedStates.Sort((x, y) => string.CompareOrdinal(x.First().Prototype?.Name, y.First().Prototype?.Name));
AddToUI(orderedStates);
_userInterfaceManager.ModalRoot.AddChild(rootContextMenu);
var size = rootContextMenu.List.DesiredSize;
var box = UIBox2.FromDimensions(_userInterfaceManager.MousePositionScaled.Position, size);
rootContextMenu.Open(box);
}
public void AddChildMenu(IEnumerable<IEntity> entities, Vector2 position, StackContextElement? stack)
{
if (stack == null) return;
var newDepth = stack.ParentMenu?.Depth + 1 ?? 1;
var childContextMenu = new ContextMenuPopup(newDepth);
Menus.Push(childContextMenu);
var orderedStates = GroupEntities(entities, newDepth);
AddToUI(orderedStates, stack);
_userInterfaceManager.ModalRoot.AddChild(childContextMenu);
var size = childContextMenu.List.DesiredSize;
childContextMenu.Open(UIBox2.FromDimensions(position + (stack.Width, 0), size));
}
private void AddToUI(List<List<IEntity>> entities, StackContextElement? stack = null)
{
if (entities.Count == 1)
{
foreach (var entity in entities[0])
{
AddSingleContextElement(entity, stack);
}
}
else
{
foreach (var entity in entities)
{
if (entity.Count == 1)
{
AddSingleContextElement(entity[0], stack);
}
else
{
AddStackContextElement(entity, stack);
}
}
}
}
private void AddSingleContextElement(IEntity entity, StackContextElement? pre)
{
if (Menus.TryPeek(out var menu))
{
var single = new SingleContextElement(entity, pre, menu);
single.OnKeyBindDown += args => OnKeyBindDownSingle?.Invoke(this, (args, single));
single.OnMouseEntered += _ => OnMouseEnteredSingle?.Invoke(this, single);
single.OnMouseExited += _ => OnMouseExitedSingle?.Invoke(this, single);
single.OnMouseHovering += () => OnMouseHoveringSingle?.Invoke(this, single);
single.OnExitedTree += () => OnExitedTree?.Invoke(this, single);
UpdateElements(entity, single);
menu.AddToMenu(single);
}
}
private void AddStackContextElement(IEnumerable<IEntity> entities, StackContextElement? pre)
{
if (Menus.TryPeek(out var menu))
{
var stack = new StackContextElement(entities, pre, menu);
stack.OnKeyBindDown += args => OnKeyBindDownStack?.Invoke(this, (args, stack));
stack.OnMouseEntered += _ => OnMouseEnteredStack?.Invoke(this, stack);
stack.OnExitedTree += () => OnExitedTree?.Invoke(this, stack);
foreach (var entity in entities)
{
UpdateElements(entity, stack);
}
menu.AddToMenu(stack);
}
}
private void UpdateElements(IEntity entity, ContextMenuElement element)
{
if (Elements.ContainsKey(entity))
{
Elements[entity] = element;
}
else
{
Elements.Add(entity, element);
}
}
private void RemoveFromUI(ContextMenuElement element)
{
var menu = element.ParentMenu;
if (menu != null)
{
menu.RemoveFromMenu(element);
if (menu.List.ChildCount == 0)
{
OnCloseChildMenu?.Invoke(this, menu.Depth - 1);
}
}
}
public void RemoveEntity(IEntity entity)
{
var element = Elements[entity];
switch (element)
{
case SingleContextElement singleContextElement:
RemoveFromUI(singleContextElement);
UpdateBranch(entity, singleContextElement.Pre);
break;
case StackContextElement stackContextElement:
stackContextElement.RemoveEntity(entity);
if (stackContextElement.EntitiesCount == 0)
{
RemoveFromUI(stackContextElement);
}
UpdateBranch(entity, stackContextElement.Pre);
break;
default:
throw new ArgumentOutOfRangeException(nameof(element));
}
Elements.Remove(entity);
}
private void UpdateBranch(IEntity entity, StackContextElement? stack)
{
while (stack != null)
{
stack.RemoveEntity(entity);
if (stack.EntitiesCount == 0)
{
RemoveFromUI(stack);
}
stack = stack.Pre;
}
}
public void UpdateParents(ContextMenuElement element)
{
switch (element)
{
case SingleContextElement singleContextElement:
if (singleContextElement.Pre != null)
{
Elements[singleContextElement.ContextEntity] = singleContextElement.Pre;
}
break;
case StackContextElement stackContextElement:
if (stackContextElement.Pre != null)
{
foreach (var entity in stackContextElement.ContextEntities)
{
Elements[entity] = stackContextElement.Pre;
}
}
break;
default:
throw new ArgumentOutOfRangeException(nameof(element));
}
}
public void CloseContextPopups()
{
while (Menus.Count > 0)
{
Menus.Pop().Dispose();
}
Elements.Clear();
}
public void CloseContextPopups(int depth)
{
while (Menus.Count > 0 && Menus.Peek().Depth > depth)
{
Menus.Pop().Dispose();
}
}
public void Dispose()
{
CloseContextPopups();
}
}
}

View File

@@ -0,0 +1,70 @@
using Content.Client.Stylesheets;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
namespace Content.Client.ContextMenu.UI
{
public partial class EntityMenuElement : ContextMenuElement
{
public const string StyleClassEntityMenuCountText = "contextMenuCount";
/// <summary>
/// The entity that can be accessed by interacting with this element.
/// </summary>
public IEntity? Entity;
/// <summary>
/// How many entities are accessible through this element's sub-menus.
/// </summary>
/// <remarks>
/// This is used for <see cref="CountLabel"/>
/// </remarks>
public int Count;
public Label CountLabel;
public SpriteView EntityIcon = new SpriteView { OverrideDirection = Direction.South};
public EntityMenuElement(IEntity? entity = null) : base()
{
CountLabel = new Label { StyleClasses = { StyleClassEntityMenuCountText } };
Icon.AddChild(new LayoutContainer() { Children = { EntityIcon, CountLabel } });
LayoutContainer.SetAnchorPreset(CountLabel, LayoutContainer.LayoutPreset.BottomRight);
LayoutContainer.SetGrowHorizontal(CountLabel, LayoutContainer.GrowDirection.Begin);
LayoutContainer.SetGrowVertical(CountLabel, LayoutContainer.GrowDirection.Begin);
Entity = entity;
if (Entity != null)
{
Count = 1;
CountLabel.Visible = false;
UpdateEntity();
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
Entity = null;
Count = 0;
}
/// <summary>
/// Update the icon and text of this element based on the given entity or this element's own entity if none
/// is provided.
/// </summary>
public void UpdateEntity(IEntity? entity = null)
{
entity ??= Entity;
EntityIcon.Sprite = entity?.GetComponentOrNull<ISpriteComponent>();
if (UserInterfaceManager.DebugMonitors.Visible)
Text = $"{entity?.Name} ({entity?.Uid})";
else
Text = entity?.Name ?? string.Empty;
}
}
}

View File

@@ -0,0 +1,358 @@
using System.Collections.Generic;
using System.Linq;
using Content.Client.Examine;
using Content.Client.Items.Managers;
using Content.Client.Verbs;
using Content.Client.Viewport;
using Content.Shared.CCVar;
using Content.Shared.Input;
using Content.Shared.Interaction.Helpers;
using Content.Shared.Verbs;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.State;
using Robust.Client.UserInterface;
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.Maths;
using Robust.Shared.Timing;
namespace Content.Client.ContextMenu.UI
{
/// <summary>
/// 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
/// 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
{
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
[Dependency] private readonly IItemSlotManager _itemSlotManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
private readonly VerbSystem _verbSystem;
/// <summary>
/// This maps the currently displayed entities to the actual GUI elements.
/// </summary>
/// <remarks>
/// This is used remove GUI elements when the entities are deleted. or leave the LOS.
/// </remarks>
public Dictionary<IEntity, EntityMenuElement> Elements = new();
public EntityMenuPresenter(VerbSystem verbSystem) : base()
{
IoCManager.InjectDependencies(this);
_verbSystem = verbSystem;
_cfg.OnValueChanged(CCVars.EntityMenuGroupingType, OnGroupingChanged, true);
CommandBinds.Builder
.Bind(ContentKeyFunctions.OpenContextMenu, new PointerInputCmdHandler(HandleOpenEntityMenu))
.Register<EntityMenuPresenter>();
}
public override void Dispose()
{
base.Dispose();
Elements.Clear();
CommandBinds.Unregister<EntityMenuPresenter>();
}
/// <summary>
/// Given a list of entities, sort them into groups and them to a new entity menu.
/// </summary>
public void OpenRootMenu(List<IEntity> entities)
{
var entitySpriteStates = GroupEntities(entities);
var orderedStates = entitySpriteStates.ToList();
orderedStates.Sort((x, y) => string.CompareOrdinal(x.First().Prototype?.Name, y.First().Prototype?.Name));
Elements.Clear();
AddToUI(orderedStates);
var box = UIBox2.FromDimensions(_userInterfaceManager.MousePositionScaled.Position, (1, 1));
RootMenu.Open(box);
}
public override void OnKeyBindDown(ContextMenuElement element, GUIBoundKeyEventArgs args)
{
base.OnKeyBindDown(element, args);
if (element is not EntityMenuElement entityElement)
return;
// get an entity associated with this element
var entity = entityElement.Entity;
entity ??= GetFirstEntityOrNull(element.SubMenu);
if (entity == null)
return;
// open verb menu?
if (args.Function == ContentKeyFunctions.OpenContextMenu)
{
_verbSystem.VerbMenu.OpenVerbMenu(entity);
args.Handle();
return;
}
// do examination?
if (args.Function == ContentKeyFunctions.ExamineEntity)
{
_systemManager.GetEntitySystem<ExamineSystem>().DoExamine(entity);
args.Handle();
return;
}
// do some other server-side interaction?
if (args.Function == EngineKeyFunctions.Use || args.Function == ContentKeyFunctions.AltActivateItemInWorld || args.Function == ContentKeyFunctions.Point ||
args.Function == ContentKeyFunctions.TryPullObject || args.Function == ContentKeyFunctions.MovePulledObject)
{
var inputSys = _systemManager.GetEntitySystem<InputSystem>();
var func = args.Function;
var funcId = _inputManager.NetworkBindMap.KeyFunctionID(func);
var message = new FullInputCmdMessage(_gameTiming.CurTick, _gameTiming.TickFraction, funcId,
BoundKeyState.Down, entity.Transform.Coordinates, args.PointerLocation, entity.Uid);
var session = _playerManager.LocalPlayer?.Session;
if (session != null)
{
inputSys.HandleInputCommand(session, func, message);
}
_verbSystem.CloseAllMenus();
args.Handle();
return;
}
if (_itemSlotManager.OnButtonPressed(args, entity))
{
_verbSystem.CloseAllMenus();
}
}
private bool HandleOpenEntityMenu(in PointerInputCmdHandler.PointerInputCmdArgs args)
{
if (args.State != BoundKeyState.Down)
return false;
if (_stateManager.CurrentState is not GameScreenBase)
return false;
var coords = args.Coordinates.ToMap(_entityManager);
if (!_verbSystem.TryGetEntityMenuEntities(coords, out var entities))
return false;
OpenRootMenu(entities);
return true;
}
/// <summary>
/// Check that entities in the context menu are still visible. If not, remove them from the context menu.
/// </summary>
public void Update()
{
if (!RootMenu.Visible)
return;
var player = _playerManager.LocalPlayer?.ControlledEntity;
if (player == null)
return;
// Do we need to do in-range unOccluded checks?
var ignoreFov = !_eyeManager.CurrentEye.DrawFov ||
(_verbSystem.Visibility & MenuVisibility.NoFov) == MenuVisibility.NoFov;
foreach (var entity in Elements.Keys.ToList())
{
if (entity.Deleted || !ignoreFov && !player.InRangeUnOccluded(entity))
RemoveEntity(entity);
}
}
/// <summary>
/// Add menu elements for a list of grouped entities;
/// </summary>
/// <param name="entityGroups"> A list of entity groups. Entities are grouped together based on prototype.</param>
private void AddToUI(List<List<IEntity>> entityGroups)
{
// If there is only a single group. We will just directly list individual entities
if (entityGroups.Count == 1)
{
foreach (var entity in entityGroups[0])
{
var element = new EntityMenuElement(entity);
AddElement(RootMenu, element);
Elements.TryAdd(entity, element);
}
return;
}
foreach (var group in entityGroups)
{
if (group.Count > 1)
{
AddGroupToUI(group);
continue;
}
// this group only has a single entity, add a simple menu element
var element = new EntityMenuElement(group[0]);
AddElement(RootMenu, element);
Elements.TryAdd(group[0], element);
}
}
/// <summary>
/// Given a group of entities, add a menu element that has a pop-up sub-menu listing group members
/// </summary>
private void AddGroupToUI(List<IEntity> group)
{
EntityMenuElement element = new();
ContextMenuPopup subMenu = new(this, element);
foreach (var entity in group)
{
var subElement = new EntityMenuElement(entity);
AddElement(subMenu, subElement);
Elements.TryAdd(entity, subElement);
}
UpdateElement(element);
AddElement(RootMenu, element);
}
/// <summary>
/// Remove an entity from the entity context menu.
/// </summary>
private void RemoveEntity(IEntity entity)
{
// find the element associated with this entity
if (!Elements.TryGetValue(entity, out var element))
{
Logger.Error($"Attempted to remove unknown entity from the entity menu: {entity.Name} ({entity.Uid})");
return;
}
// remove the element
var parent = element.ParentMenu?.ParentElement;
element.Dispose();
Elements.Remove(entity);
// update any parent elements
if (parent is EntityMenuElement e)
UpdateElement(e);
// if the verb menu is open and targeting this entity, close it.
if (_verbSystem.VerbMenu.CurrentTarget == entity.Uid)
_verbSystem.VerbMenu.Close();
// If this was the last entity, close the entity menu
if (RootMenu.MenuBody.ChildCount == 0)
Close();
}
/// <summary>
/// Update the information displayed by a menu element.
/// </summary>
/// <remarks>
/// This is called when initializing elements or after an element was removed from a sub-menu.
/// </remarks>
private void UpdateElement(EntityMenuElement element)
{
if (element.SubMenu == null)
return;
// Get the first entity in the sub-menus
var entity = GetFirstEntityOrNull(element.SubMenu);
if (entity == null)
{
// This whole element has no associated entities. We should remove it
element.Dispose();
return;
}
element.UpdateEntity(entity);
// Update the entity count & count label
element.Count = 0;
foreach (var subElement in element.SubMenu.MenuBody.Children)
{
if (subElement is EntityMenuElement entityElement)
element.Count += entityElement.Count;
}
element.CountLabel.Text = element.Count.ToString();
if (element.Count == 1)
{
// There was only one entity in the sub-menu. So we will just remove the sub-menu and point directly to
// that entity.
element.Entity = entity;
element.SubMenu.Dispose();
element.SubMenu = null;
element.CountLabel.Visible = false;
Elements[entity] = element;
}
// update the parent element, so that it's count and entity icon gets updated.
var parent = element.ParentMenu?.ParentElement;
if (parent is EntityMenuElement e)
UpdateElement(e);
}
/// <summary>
/// Look through a sub-menu and return the first entity.
/// </summary>
private IEntity? GetFirstEntityOrNull(ContextMenuPopup? menu)
{
if (menu == null)
return null;
foreach (var element in menu.MenuBody.Children)
{
if (element is not EntityMenuElement entityElement)
continue;
if (entityElement.Entity != null)
return entityElement.Entity;
var entity = GetFirstEntityOrNull(entityElement.SubMenu);
if (entity != null)
return entity;
}
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

@@ -6,13 +6,13 @@ using Robust.Shared.GameObjects;
namespace Content.Client.ContextMenu.UI
{
public partial class ContextMenuView
public sealed partial class EntityMenuPresenter : ContextMenuPresenter
{
public const int GroupingTypesCount = 2;
private int GroupingContextMenuType { get; set; }
public void OnGroupingContextMenuChanged(int obj)
public void OnGroupingChanged(int obj)
{
CloseContextPopups();
Close();
GroupingContextMenuType = obj;
}