From 49296e33a09fd68c746c5d63db97cb3114446127 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Thu, 28 Oct 2021 18:21:19 +1300 Subject: [PATCH] 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 --- ...ommand.cs => GroupingEntityMenuCommand.cs} | 15 +- .../Commands/SetMenuVisibilityCommand.cs | 54 ++ .../ContextMenu/UI/ContextMenuElement.cs | 249 --------- .../ContextMenu/UI/ContextMenuElement.xaml | 26 + .../ContextMenu/UI/ContextMenuElement.xaml.cs | 101 ++++ .../ContextMenu/UI/ContextMenuPopup.xaml | 6 + .../ContextMenu/UI/ContextMenuPopup.xaml.cs | 70 +++ .../ContextMenu/UI/ContextMenuPresenter.cs | 488 +++++------------- .../ContextMenu/UI/ContextMenuView.cs | 263 ---------- .../ContextMenu/UI/EntityMenuElement.cs | 70 +++ .../ContextMenu/UI/EntityMenuPresenter.cs | 358 +++++++++++++ ...ping.cs => EntityMenuPresenterGrouping.cs} | 6 +- Content.Client/Examine/ExamineSystem.cs | 1 + .../Items/Managers/ItemSlotManager.cs | 3 +- Content.Client/Stylesheets/StyleBase.cs | 20 +- Content.Client/Stylesheets/StyleNano.cs | 59 ++- Content.Client/Verbs/UI/VerbMenuElement.cs | 69 +++ Content.Client/Verbs/UI/VerbMenuPresenter.cs | 200 +++++++ Content.Client/Verbs/VerbMenuElement.cs | 246 --------- Content.Client/Verbs/VerbSystem.cs | 303 ++++++----- Content.Client/Viewport/GameScreenBase.cs | 24 +- .../Administration/AdminVerbSystem.cs | 4 +- Content.Server/Buckle/Systems/BuckleSystem.cs | 3 +- Content.Server/Cabinet/ItemCabinetSystem.cs | 4 +- .../EntitySystems/HandHeldLightSystem.cs | 2 +- .../Storage/EntitySystems/StorageSystem.cs | 8 +- .../Verbs/ToggleAllContextCommand.cs | 32 -- Content.Server/Verbs/VerbSystem.cs | 90 +--- Content.Shared/CCVar/CCVars.cs | 4 +- .../Interaction/SharedInteractionSystem.cs | 2 +- .../Item}/ItemSystem.cs | 5 +- Content.Shared/Verbs/SharedVerbSystem.cs | 105 +--- Content.Shared/Verbs/Verb.cs | 39 +- Content.Shared/Verbs/VerbCategory.cs | 3 +- Content.Shared/Verbs/VerbEvents.cs | 13 +- Resources/Locale/en-US/verbs/verb-system.ftl | 11 +- 36 files changed, 1421 insertions(+), 1535 deletions(-) rename Content.Client/Commands/{GroupingContextMenuCommand.cs => GroupingEntityMenuCommand.cs} (65%) create mode 100644 Content.Client/Commands/SetMenuVisibilityCommand.cs delete mode 100644 Content.Client/ContextMenu/UI/ContextMenuElement.cs create mode 100644 Content.Client/ContextMenu/UI/ContextMenuElement.xaml create mode 100644 Content.Client/ContextMenu/UI/ContextMenuElement.xaml.cs create mode 100644 Content.Client/ContextMenu/UI/ContextMenuPopup.xaml create mode 100644 Content.Client/ContextMenu/UI/ContextMenuPopup.xaml.cs delete mode 100644 Content.Client/ContextMenu/UI/ContextMenuView.cs create mode 100644 Content.Client/ContextMenu/UI/EntityMenuElement.cs create mode 100644 Content.Client/ContextMenu/UI/EntityMenuPresenter.cs rename Content.Client/ContextMenu/UI/{ContextMenuViewGrouping.cs => EntityMenuPresenterGrouping.cs} (96%) create mode 100644 Content.Client/Verbs/UI/VerbMenuElement.cs create mode 100644 Content.Client/Verbs/UI/VerbMenuPresenter.cs delete mode 100644 Content.Client/Verbs/VerbMenuElement.cs delete mode 100644 Content.Server/Verbs/ToggleAllContextCommand.cs rename {Content.Server/Items => Content.Shared/Item}/ItemSystem.cs (94%) diff --git a/Content.Client/Commands/GroupingContextMenuCommand.cs b/Content.Client/Commands/GroupingEntityMenuCommand.cs similarity index 65% rename from Content.Client/Commands/GroupingContextMenuCommand.cs rename to Content.Client/Commands/GroupingEntityMenuCommand.cs index 1b640b0907..99efb93c71 100644 --- a/Content.Client/Commands/GroupingContextMenuCommand.cs +++ b/Content.Client/Commands/GroupingEntityMenuCommand.cs @@ -1,19 +1,18 @@ -using Content.Shared; +using Content.Client.ContextMenu.UI; using Content.Shared.CCVar; using Robust.Shared.Configuration; using Robust.Shared.Console; using Robust.Shared.IoC; -using ContextMenuView = Content.Client.ContextMenu.UI.ContextMenuView; namespace Content.Client.Commands { - public class GroupingContextMenuCommand : IConsoleCommand + public class GroupingEntityMenuCommand : IConsoleCommand { - public string Command => "contextmenug"; + public string Command => "entitymenug"; - public string Description => "Sets the contextmenu-groupingtype."; + public string Description => "Sets the entity menu grouping type."; - public string Help => ($"Usage: contextmenug <0:{ContextMenuView.GroupingTypesCount}>"); + public string Help => $"Usage: entitymenug <0:{EntityMenuPresenter.GroupingTypesCount}>"; public void Execute(IConsoleShell shell, string argStr, string[] args) { if (args.Length != 1) @@ -28,14 +27,14 @@ namespace Content.Client.Commands return; } - if (id < 0 ||id > ContextMenuView.GroupingTypesCount - 1) + if (id < 0 ||id > EntityMenuPresenter.GroupingTypesCount - 1) { shell.WriteLine($"{args[0]} is not a valid integer."); return; } var configurationManager = IoCManager.Resolve(); - var cvar = CCVars.ContextMenuGroupingType; + var cvar = CCVars.EntityMenuGroupingType; configurationManager.SetCVar(cvar, id); shell.WriteLine($"Context Menu Grouping set to type: {configurationManager.GetCVar(cvar)}"); diff --git a/Content.Client/Commands/SetMenuVisibilityCommand.cs b/Content.Client/Commands/SetMenuVisibilityCommand.cs new file mode 100644 index 0000000000..91df58a374 --- /dev/null +++ b/Content.Client/Commands/SetMenuVisibilityCommand.cs @@ -0,0 +1,54 @@ +using Content.Client.Verbs; +using JetBrains.Annotations; +using Robust.Shared.Console; +using Robust.Shared.GameObjects; + +namespace Content.Client.Commands +{ + [UsedImplicitly] + internal sealed class SetMenuVisibilityCommand : IConsoleCommand + { + public const string CommandName = "menuvis"; + + public string Command => CommandName; + public string Description => "Set restrictions about what entities to show on the entity context menu."; + public string Help => $"Usage: {Command} [NoFoV] [InContainer] [Invisible] [All]"; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (!TryParseArguments(shell, args, out var visibility)) + return; + + EntitySystem.Get().Visibility = visibility; + } + + private bool TryParseArguments(IConsoleShell shell, string[] args, out MenuVisibility visibility) + { + visibility = MenuVisibility.Default; + + foreach (var arg in args) + { + switch (arg.ToLower()) + { + case "nofov": + visibility |= MenuVisibility.NoFov; + break; + case "incontainer": + visibility |= MenuVisibility.InContainer; + break; + case "invisible": + visibility |= MenuVisibility.Invisible; + break; + case "all": + visibility |= MenuVisibility.All; + break; + default: + shell.WriteLine($"Unknown visibility argument '{arg}'. Only 'NoFov', 'InContainer', 'Invisible' or 'All' are valid. Provide no arguments to set to default."); + return false; + } + } + + return true; + } + } +} diff --git a/Content.Client/ContextMenu/UI/ContextMenuElement.cs b/Content.Client/ContextMenu/UI/ContextMenuElement.cs deleted file mode 100644 index b1d3b909ec..0000000000 --- a/Content.Client/ContextMenu/UI/ContextMenuElement.cs +++ /dev/null @@ -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(); - - 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 ContextEntities { get; } - public readonly StackContextElement? Pre; - - private readonly SpriteView _spriteView; - private readonly Label _label; - - public int EntitiesCount => ContextEntities.Count; - - public StackContextElement(IEnumerable entities, StackContextElement? pre, ContextMenuPopup? parentMenu) - : base(parentMenu) - { - Pre = pre; - ContextEntities = new(entities); - _spriteView = new SpriteView - { - Sprite = ContextEntities.First().GetComponent() - }; - _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().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(); - } - } - - 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; - } - } -} diff --git a/Content.Client/ContextMenu/UI/ContextMenuElement.xaml b/Content.Client/ContextMenu/UI/ContextMenuElement.xaml new file mode 100644 index 0000000000..6b82394ab6 --- /dev/null +++ b/Content.Client/ContextMenu/UI/ContextMenuElement.xaml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/Content.Client/ContextMenu/UI/ContextMenuElement.xaml.cs b/Content.Client/ContextMenu/UI/ContextMenuElement.xaml.cs new file mode 100644 index 0000000000..c186e27fd5 --- /dev/null +++ b/Content.Client/ContextMenu/UI/ContextMenuElement.xaml.cs @@ -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 +{ + /// + /// 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. + /// + [GenerateTypedNameReferences] + public partial class ContextMenuElement : ContainerButton + { + public const string StyleClassContextMenuButton = "contextMenuButton"; + + public const float ElementMargin = 2; + public const float ElementHeight = 32; + + /// + /// The menu that contains this element + /// + public ContextMenuPopup? ParentMenu; + + private ContextMenuPopup? _subMenu; + + /// + /// The pop-up menu that is opened when hovering over this element. + /// + public ContextMenuPopup? SubMenu + { + get => _subMenu; + set + { + _subMenu = value; + ExpansionIndicator.Visible = _subMenu != null; + } + } + + /// + /// Convenience property to set label text. + /// + 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() + .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); + } + + /// + /// If this element's sub-menu is currently visible, give it the hovered pseudo class. + /// + /// + /// 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. + /// + 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); + } + } +} diff --git a/Content.Client/ContextMenu/UI/ContextMenuPopup.xaml b/Content.Client/ContextMenu/UI/ContextMenuPopup.xaml new file mode 100644 index 0000000000..c450b46681 --- /dev/null +++ b/Content.Client/ContextMenu/UI/ContextMenuPopup.xaml @@ -0,0 +1,6 @@ + + + + + + diff --git a/Content.Client/ContextMenu/UI/ContextMenuPopup.xaml.cs b/Content.Client/ContextMenu/UI/ContextMenuPopup.xaml.cs new file mode 100644 index 0000000000..7376278aa4 --- /dev/null +++ b/Content.Client/ContextMenu/UI/ContextMenuPopup.xaml.cs @@ -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 +{ + /// + /// The base context-menu pop-up window used for both the entity and verb menus. + /// + [GenerateTypedNameReferences] + public partial class ContextMenuPopup : Popup + { + public const string StyleClassContextMenuPopup = "contextMenuPopup"; + + /// + /// How many items to list before limiting the size and adding a scroll bar. + /// + public const int MaxItemsBeforeScroll = 10; + + /// + /// If this pop-up is created by hovering over some element in another pop-up, this is that element. + /// + public ContextMenuElement? ParentElement; + + /// + /// This is the main body of the menu. The menu entries should be added to this object. + /// + 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(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); + } + } +} diff --git a/Content.Client/ContextMenu/UI/ContextMenuPresenter.cs b/Content.Client/ContextMenu/UI/ContextMenuPresenter.cs index 753f6c611b..1ad64a4f8c 100644 --- a/Content.Client/ContextMenu/UI/ContextMenuPresenter.cs +++ b/Content.Client/ContextMenu/UI/ContextMenuPresenter.cs @@ -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 { + /// + /// This class handles all the logic associated with showing a context menu. + /// + /// + /// This largely involves setting up timers to open and close sub-menus when hovering over other menu elements. + /// 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 Menus { get; } = new(); - private MapCoordinates _mapCoordinates; + /// + /// Used to cancel the timer that opens menus. + /// + public CancellationTokenSource? CancelOpen; - public ContextMenuPresenter(VerbSystem verbSystem) + /// + /// Used to cancel the timer that closes menus. + /// + 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(); - } - - #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(); - - 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().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(); - - 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); } /// - /// Can the player see the entity through any entity containers? + /// Dispose of all UI elements. + /// + public virtual void Dispose() + { + RootMenu.OnPopupHide -= RootMenu.MenuBody.DisposeAllChildren; + RootMenu.Dispose(); + } + + /// + /// Close and clear the root menu. This will also dispose any sub-menus. + /// + public virtual void Close() + { + RootMenu.Close(); + CancelOpen?.Cancel(); + CancelClose?.Cancel(); + } + + /// + /// Starts closing menus until the top-most menu is the given one. /// /// - /// This is similar to , 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. /// - 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(); + } } /// - /// 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. /// - 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) + /// + /// Start a timer to close this element's sub-menu. + /// + /// + /// Note that this timer will be aborted when entering the actual sub-menu itself. + /// + 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) { } + + /// + /// Opens a new sub menu, and close the old one. + /// + /// + /// If the given element has no sub-menu, just close the current one. + /// + 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(); + 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); + } + + /// + /// Add an element to a menu and subscribe to GUI events. + /// + 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(); + } + + /// + /// Removes event subscriptions when an element is removed from a menu, + /// + 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(); } } } diff --git a/Content.Client/ContextMenu/UI/ContextMenuView.cs b/Content.Client/ContextMenu/UI/ContextMenuView.cs deleted file mode 100644 index 1f25206931..0000000000 --- a/Content.Client/ContextMenu/UI/ContextMenuView.cs +++ /dev/null @@ -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 Elements { get; set; } - Stack Menus { get; } - event EventHandler<(GUIBoundKeyEventArgs, SingleContextElement)>? OnKeyBindDownSingle; - event EventHandler? OnMouseEnteredSingle; - event EventHandler? OnMouseExitedSingle; - event EventHandler? OnMouseHoveringSingle; - - event EventHandler<(GUIBoundKeyEventArgs, StackContextElement)>? OnKeyBindDownStack; - event EventHandler? OnMouseEnteredStack; - - event EventHandler? OnExitedTree; - - event EventHandler? OnCloseRootMenu; - event EventHandler? OnCloseChildMenu; - - void UpdateParents(ContextMenuElement element); - void RemoveEntity(IEntity element); - void AddRootMenu(List entities); - void AddChildMenu(IEnumerable 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 Menus { get; } - public Dictionary Elements { get; set; } - - public event EventHandler<(GUIBoundKeyEventArgs, SingleContextElement)>? OnKeyBindDownSingle; - public event EventHandler? OnMouseEnteredSingle; - public event EventHandler? OnMouseExitedSingle; - public event EventHandler? OnMouseHoveringSingle; - - public event EventHandler<(GUIBoundKeyEventArgs, StackContextElement)>? OnKeyBindDownStack; - public event EventHandler? OnMouseEnteredStack; - - public event EventHandler? OnExitedTree; - - public event EventHandler? OnCloseRootMenu; - public event EventHandler? OnCloseChildMenu; - - public ContextMenuView() - { - IoCManager.InjectDependencies(this); - Menus = new Stack(); - Elements = new Dictionary(); - } - - public void AddRootMenu(List entities) - { - Elements = new Dictionary(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 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> 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 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(); - } - } -} diff --git a/Content.Client/ContextMenu/UI/EntityMenuElement.cs b/Content.Client/ContextMenu/UI/EntityMenuElement.cs new file mode 100644 index 0000000000..40f386c451 --- /dev/null +++ b/Content.Client/ContextMenu/UI/EntityMenuElement.cs @@ -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"; + + /// + /// The entity that can be accessed by interacting with this element. + /// + public IEntity? Entity; + + /// + /// How many entities are accessible through this element's sub-menus. + /// + /// + /// This is used for + /// + 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; + } + + /// + /// Update the icon and text of this element based on the given entity or this element's own entity if none + /// is provided. + /// + public void UpdateEntity(IEntity? entity = null) + { + entity ??= Entity; + + EntityIcon.Sprite = entity?.GetComponentOrNull(); + + if (UserInterfaceManager.DebugMonitors.Visible) + Text = $"{entity?.Name} ({entity?.Uid})"; + else + Text = entity?.Name ?? string.Empty; + } + } +} diff --git a/Content.Client/ContextMenu/UI/EntityMenuPresenter.cs b/Content.Client/ContextMenu/UI/EntityMenuPresenter.cs new file mode 100644 index 0000000000..76dac3ae9e --- /dev/null +++ b/Content.Client/ContextMenu/UI/EntityMenuPresenter.cs @@ -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 +{ + /// + /// This class handles the displaying of the entity context menu. + /// + /// + /// In addition to the normal 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. + /// + 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; + + /// + /// This maps the currently displayed entities to the actual GUI elements. + /// + /// + /// This is used remove GUI elements when the entities are deleted. or leave the LOS. + /// + public Dictionary 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(); + } + + public override void Dispose() + { + base.Dispose(); + Elements.Clear(); + CommandBinds.Unregister(); + } + + /// + /// Given a list of entities, sort them into groups and them to a new entity menu. + /// + public void OpenRootMenu(List 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().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(); + + 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; + } + + /// + /// Check that entities in the context menu are still visible. If not, remove them from the context menu. + /// + 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); + } + } + + /// + /// Add menu elements for a list of grouped entities; + /// + /// A list of entity groups. Entities are grouped together based on prototype. + private void AddToUI(List> 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); + } + + } + + /// + /// Given a group of entities, add a menu element that has a pop-up sub-menu listing group members + /// + private void AddGroupToUI(List 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); + } + + /// + /// Remove an entity from the entity context menu. + /// + 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(); + } + + /// + /// Update the information displayed by a menu element. + /// + /// + /// This is called when initializing elements or after an element was removed from a sub-menu. + /// + 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); + } + + /// + /// Look through a sub-menu and return the first entity. + /// + 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(); + } + } + } +} diff --git a/Content.Client/ContextMenu/UI/ContextMenuViewGrouping.cs b/Content.Client/ContextMenu/UI/EntityMenuPresenterGrouping.cs similarity index 96% rename from Content.Client/ContextMenu/UI/ContextMenuViewGrouping.cs rename to Content.Client/ContextMenu/UI/EntityMenuPresenterGrouping.cs index 8175d18431..00fe9f64ce 100644 --- a/Content.Client/ContextMenu/UI/ContextMenuViewGrouping.cs +++ b/Content.Client/ContextMenu/UI/EntityMenuPresenterGrouping.cs @@ -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; } diff --git a/Content.Client/Examine/ExamineSystem.cs b/Content.Client/Examine/ExamineSystem.cs index 4610587e26..8ec345f984 100644 --- a/Content.Client/Examine/ExamineSystem.cs +++ b/Content.Client/Examine/ExamineSystem.cs @@ -98,6 +98,7 @@ namespace Content.Client.Examine verb.Act = () => DoExamine(args.Target) ; verb.Text = Loc.GetString("examine-verb-name"); verb.IconTexture = "/Textures/Interface/VerbIcons/examine.svg.192dpi.png"; + verb.ClientExclusive = true; args.Verbs.Add(verb); } diff --git a/Content.Client/Items/Managers/ItemSlotManager.cs b/Content.Client/Items/Managers/ItemSlotManager.cs index e131cb6b5a..47277f8b61 100644 --- a/Content.Client/Items/Managers/ItemSlotManager.cs +++ b/Content.Client/Items/Managers/ItemSlotManager.cs @@ -77,8 +77,7 @@ namespace Content.Client.Items.Managers } else if (args.Function == ContentKeyFunctions.OpenContextMenu) { - _entitySystemManager.GetEntitySystem() - .OpenVerbMenu(item, _uiMgr.ScreenToUIPosition(args.PointerLocation)); + _entitySystemManager.GetEntitySystem().VerbMenu.OpenVerbMenu(item); } else if (args.Function == ContentKeyFunctions.ActivateItemInWorld) { diff --git a/Content.Client/Stylesheets/StyleBase.cs b/Content.Client/Stylesheets/StyleBase.cs index ada3880648..2193afe523 100644 --- a/Content.Client/Stylesheets/StyleBase.cs +++ b/Content.Client/Stylesheets/StyleBase.cs @@ -25,6 +25,8 @@ namespace Content.Client.Stylesheets public const string ButtonCaution = "Caution"; + public const int DefaultGrabberSize = 10; + public abstract Stylesheet Stylesheet { get; } protected StyleRule[] BaseRules { get; } @@ -96,31 +98,31 @@ namespace Content.Client.Stylesheets var vScrollBarGrabberNormal = new StyleBoxFlat { - BackgroundColor = Color.Gray.WithAlpha(0.35f), ContentMarginLeftOverride = 10, - ContentMarginTopOverride = 10 + BackgroundColor = Color.Gray.WithAlpha(0.35f), ContentMarginLeftOverride = DefaultGrabberSize, + ContentMarginTopOverride = DefaultGrabberSize }; var vScrollBarGrabberHover = new StyleBoxFlat { - BackgroundColor = new Color(140, 140, 140).WithAlpha(0.35f), ContentMarginLeftOverride = 10, - ContentMarginTopOverride = 10 + BackgroundColor = new Color(140, 140, 140).WithAlpha(0.35f), ContentMarginLeftOverride = DefaultGrabberSize, + ContentMarginTopOverride = DefaultGrabberSize }; var vScrollBarGrabberGrabbed = new StyleBoxFlat { - BackgroundColor = new Color(160, 160, 160).WithAlpha(0.35f), ContentMarginLeftOverride = 10, - ContentMarginTopOverride = 10 + BackgroundColor = new Color(160, 160, 160).WithAlpha(0.35f), ContentMarginLeftOverride = DefaultGrabberSize, + ContentMarginTopOverride = DefaultGrabberSize }; var hScrollBarGrabberNormal = new StyleBoxFlat { - BackgroundColor = Color.Gray.WithAlpha(0.35f), ContentMarginTopOverride = 10 + BackgroundColor = Color.Gray.WithAlpha(0.35f), ContentMarginTopOverride = DefaultGrabberSize }; var hScrollBarGrabberHover = new StyleBoxFlat { - BackgroundColor = new Color(140, 140, 140).WithAlpha(0.35f), ContentMarginTopOverride = 10 + BackgroundColor = new Color(140, 140, 140).WithAlpha(0.35f), ContentMarginTopOverride = DefaultGrabberSize }; var hScrollBarGrabberGrabbed = new StyleBoxFlat { - BackgroundColor = new Color(160, 160, 160).WithAlpha(0.35f), ContentMarginTopOverride = 10 + BackgroundColor = new Color(160, 160, 160).WithAlpha(0.35f), ContentMarginTopOverride = DefaultGrabberSize }; diff --git a/Content.Client/Stylesheets/StyleNano.cs b/Content.Client/Stylesheets/StyleNano.cs index 4674d2afaa..673f82fe67 100644 --- a/Content.Client/Stylesheets/StyleNano.cs +++ b/Content.Client/Stylesheets/StyleNano.cs @@ -1,11 +1,13 @@ using System.Linq; using Content.Client.Actions.UI; +using Content.Client.ContextMenu.UI; using Content.Client.Examine; using Content.Client.HUD; using Content.Client.HUD.UI; using Content.Client.Resources; using Content.Client.Targeting; using Content.Client.UserInterface.Controls; +using Content.Client.Verbs.UI; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; using Robust.Client.UserInterface; @@ -38,7 +40,6 @@ namespace Content.Client.Stylesheets public const string StyleClassChatLineEdit = "chatLineEdit"; public const string StyleClassChatChannelSelectorButton = "chatSelectorOptionButton"; public const string StyleClassChatFilterOptionButton = "chatFilterOptionButton"; - public const string StyleClassContextMenuCount = "contextMenuCount"; public const string StyleClassStorageButton = "storageButton"; public const string StyleClassSliderRed = "Red"; @@ -66,6 +67,12 @@ namespace Content.Client.Stylesheets public static readonly Color ButtonColorCautionPressed = Color.FromHex("#3e6c45"); public static readonly Color ButtonColorCautionDisabled = Color.FromHex("#602a2a"); + // Context menu button colors + public static readonly Color ButtonColorContext = Color.FromHex("#1119"); + public static readonly Color ButtonColorContextHover = Color.DarkSlateGray; + public static readonly Color ButtonColorContextPressed = Color.LightSlateGray; + public static readonly Color ButtonColorContextDisabled = Color.Black; + //Used by the APC and SMES menus public const string StyleClassPowerStateNone = "PowerStateNone"; public const string StyleClassPowerStateLow = "PowerStateLow"; @@ -82,6 +89,7 @@ namespace Content.Client.Stylesheets var notoSans12 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 12); var notoSansItalic12 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Italic.ttf", 12); var notoSansBold12 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 12); + var notoSansBoldItalic12 = resCache.GetFont("/Fonts/NotoSans/NotoSans-BoldItalic.ttf", 12); var notoSansDisplayBold14 = resCache.GetFont("/Fonts/NotoSansDisplay/NotoSansDisplay-Bold.ttf", 14); var notoSansDisplayBold16 = resCache.GetFont("/Fonts/NotoSansDisplay/NotoSansDisplay-Bold.ttf", 16); var notoSans15 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 15); @@ -112,6 +120,12 @@ namespace Content.Client.Stylesheets }; borderedWindowBackground.SetPatchMargin(StyleBox.Margin.All, 2); + var contextMenuBackground = new StyleBoxTexture + { + Texture = borderedWindowBackgroundTex, + }; + contextMenuBackground.SetPatchMargin(StyleBox.Margin.All, ContextMenuElement.ElementMargin); + var invSlotBgTex = resCache.GetTexture("/Textures/Interface/Inventory/inv_slot_background.png"); var invSlotBg = new StyleBoxTexture { @@ -147,6 +161,8 @@ namespace Content.Client.Stylesheets buttonStorage.SetContentMarginOverride(StyleBox.Margin.Vertical, 0); buttonStorage.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4); + var buttonContext = new StyleBoxTexture { Texture = Texture.White }; + var buttonRectTex = resCache.GetTexture("/Textures/Interface/Nano/light_panel_background_bordered.png"); var buttonRect = new StyleBoxTexture(BaseButton) { @@ -504,6 +520,43 @@ namespace Content.Client.Stylesheets new StyleProperty("font-color", Color.FromHex("#E5E5E581")), }), + // Context Menu window + Element().Class(ContextMenuPopup.StyleClassContextMenuPopup) + .Prop(PanelContainer.StylePropertyPanel, contextMenuBackground), + + // Context menu buttons + Element().Class(ContextMenuElement.StyleClassContextMenuButton) + .Prop(ContainerButton.StylePropertyStyleBox, buttonContext), + + Element().Class(ContextMenuElement.StyleClassContextMenuButton) + .Pseudo(ContainerButton.StylePseudoClassNormal) + .Prop(Control.StylePropertyModulateSelf, ButtonColorContext), + + Element().Class(ContextMenuElement.StyleClassContextMenuButton) + .Pseudo(ContainerButton.StylePseudoClassHover) + .Prop(Control.StylePropertyModulateSelf, ButtonColorContextHover), + + Element().Class(ContextMenuElement.StyleClassContextMenuButton) + .Pseudo(ContainerButton.StylePseudoClassPressed) + .Prop(Control.StylePropertyModulateSelf, ButtonColorContextPressed), + + Element().Class(ContextMenuElement.StyleClassContextMenuButton) + .Pseudo(ContainerButton.StylePseudoClassDisabled) + .Prop(Control.StylePropertyModulateSelf, ButtonColorContextDisabled), + + // Context Menu Labels + Element().Class(VerbMenuElement.StyleClassVerbInteractionText) + .Prop(Label.StylePropertyFont, notoSansBoldItalic12), + + Element().Class(VerbMenuElement.StyleClassVerbActivationText) + .Prop(Label.StylePropertyFont, notoSansBold12), + + Element().Class(VerbMenuElement.StyleClassVerbAlternativeText) + .Prop(Label.StylePropertyFont, notoSansItalic12), + + Element().Class(VerbMenuElement.StyleClassVerbOtherText) + .Prop(Label.StylePropertyFont, notoSans12), + // Thin buttons (No padding nor vertical margin) Element().Class(StyleClassStorageButton) .Prop(ContainerButton.StylePropertyStyleBox, buttonStorage), @@ -708,8 +761,8 @@ namespace Content.Client.Stylesheets new StyleProperty("font", notoSans15) }), - // small number for the context menu - new StyleRule(new SelectorElement(typeof(Label), new[] {StyleClassContextMenuCount}, null, null), new[] + // small number for the entity counter in the entity menu + new StyleRule(new SelectorElement(typeof(Label), new[] {EntityMenuElement.StyleClassEntityMenuCountText}, null, null), new[] { new StyleProperty("font", notoSans10), new StyleProperty(Label.StylePropertyAlignMode, Label.AlignMode.Right), diff --git a/Content.Client/Verbs/UI/VerbMenuElement.cs b/Content.Client/Verbs/UI/VerbMenuElement.cs new file mode 100644 index 0000000000..ab6259f4dd --- /dev/null +++ b/Content.Client/Verbs/UI/VerbMenuElement.cs @@ -0,0 +1,69 @@ +using Content.Client.ContextMenu.UI; +using Content.Shared.Verbs; +using Robust.Client.UserInterface.Controls; +using Robust.Client.Utility; +using Robust.Shared.Utility; + +namespace Content.Client.Verbs.UI +{ + /// + /// Slight extension of that uses a SpriteSpecifier for it's icon and provides + /// constructors that take verbs or verb categories. + /// + public partial class VerbMenuElement : ContextMenuElement + { + public const string StyleClassVerbInteractionText = "InteractionVerb"; + public const string StyleClassVerbActivationText = "ActivationVerb"; + public const string StyleClassVerbAlternativeText = "AlternativeVerb"; + public const string StyleClassVerbOtherText = "OtherVerb"; + + public const float VerbTooltipDelay = 0.5f; + + // Setters to provide access to children generated by XAML. + public bool IconVisible { set => Icon.Visible = value; } + public bool TextVisible { set => Label.Visible = value; } + + // Top quality variable naming + public Verb? Verb; + + public VerbType Type; + + public VerbMenuElement(string? text, SpriteSpecifier? icon, VerbType verbType) : base(text) + { + Icon.AddChild(new TextureRect() + { + Texture = icon?.Frame0(), + Stretch = TextureRect.StretchMode.KeepAspectCentered + }); + + Type = verbType; + + // Set text font style based on verb type + switch (verbType) + { + case VerbType.Interaction: + Label.SetOnlyStyleClass(StyleClassVerbInteractionText); + break; + case VerbType.Activation: + Label.SetOnlyStyleClass(StyleClassVerbActivationText); + break; + case VerbType.Alternative: + Label.SetOnlyStyleClass(StyleClassVerbAlternativeText); + break; + default: + Label.SetOnlyStyleClass(StyleClassVerbOtherText); + break; + } + } + + public VerbMenuElement(Verb verb, VerbType verbType) : this(verb.Text, verb.Icon, verbType) + { + ToolTip = verb.Message; + TooltipDelay = VerbTooltipDelay; + Disabled = verb.Disabled; + Verb = verb; + } + + public VerbMenuElement(VerbCategory category, VerbType verbType) : this(category.Text, category.Icon, verbType) { } + } +} diff --git a/Content.Client/Verbs/UI/VerbMenuPresenter.cs b/Content.Client/Verbs/UI/VerbMenuPresenter.cs new file mode 100644 index 0000000000..20d4ad9965 --- /dev/null +++ b/Content.Client/Verbs/UI/VerbMenuPresenter.cs @@ -0,0 +1,200 @@ +using System.Collections.Generic; +using System.Linq; +using Content.Client.ContextMenu.UI; +using Content.Shared.Input; +using Content.Shared.Verbs; +using Robust.Client.Player; +using Robust.Client.UserInterface; +using Robust.Shared.GameObjects; +using Robust.Shared.Input; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Maths; +using static Robust.Client.UserInterface.Controls.BoxContainer; + +namespace Content.Client.Verbs.UI +{ + /// + /// This class handles the displaying of the verb menu. + /// + /// + /// In addition to the normal 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. + /// + public sealed class VerbMenuPresenter : ContextMenuPresenter + { + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; + + private readonly VerbSystem _verbSystem; + + public EntityUid CurrentTarget; + public Dictionary> CurrentVerbs = new(); + + public VerbMenuPresenter(VerbSystem verbSystem) : base() + { + IoCManager.InjectDependencies(this); + _verbSystem = verbSystem; + } + + /// + /// Open a verb menu and fill it work verbs applicable to the given target entity. + /// + public void OpenVerbMenu(IEntity target) + { + var user = _playerManager.LocalPlayer?.ControlledEntity; + if (user == null) + return; + + Close(); + + CurrentTarget = target.Uid; + CurrentVerbs = _verbSystem.GetVerbs(target, user, VerbType.All); + + if (!target.Uid.IsClientSide()) + { + AddElement(RootMenu, new ContextMenuElement(Loc.GetString("verb-system-waiting-on-server-text"))); + } + + // Show the menu + FillVerbPopup(); + RootMenu.SetPositionLast(); + var box = UIBox2.FromDimensions(_userInterfaceManager.MousePositionScaled.Position, (1, 1)); + RootMenu.Open(box); + } + + /// + /// Fill the verb pop-up using the verbs stored in + /// + private void FillVerbPopup() + { + if (RootMenu == null) + return; + + // Add verbs to pop-up, grouped by type. Order determined by how types are defined VerbTypes + var types = CurrentVerbs.Keys.ToList(); + types.Sort(); + foreach (var type in types) + { + if (!CurrentVerbs.TryGetValue(type, out var verbs)) + continue; + + HashSet listedCategories = new(); + foreach (var verb in verbs) + { + if (verb.Category == null) + { + var element = new VerbMenuElement(verb, type); + AddElement(RootMenu, element); + } + + else if (listedCategories.Add(verb.Category.Text)) + AddVerbCategory(verb.Category, verbs, type); + } + } + + RootMenu.InvalidateMeasure(); + } + + /// + /// Add a verb category button to the pop-up + /// + public void AddVerbCategory(VerbCategory category, SortedSet verbs, VerbType type) + { + // Get a list of the verbs in this category + List verbsInCategory = new(); + var drawIcons = false; + foreach (var verb in verbs) + { + if (verb.Category?.Text == category.Text) + { + verbsInCategory.Add(verb); + drawIcons = drawIcons || verb.Icon != null; + } + } + + if (verbsInCategory.Count == 0) + return; + + var element = new VerbMenuElement(category, type); + AddElement(RootMenu, element); + + // Create the pop-up that appears when hovering over this element + element.SubMenu = new ContextMenuPopup(this, element); + foreach (var verb in verbsInCategory) + { + var subElement = new VerbMenuElement(verb, type) + { + IconVisible = drawIcons, + TextVisible = !category.IconsOnly + }; + AddElement(element.SubMenu, subElement); + } + + if (category.IconsOnly) + element.SubMenu.MenuBody.Orientation = LayoutOrientation.Horizontal; + } + + /// + /// Add verbs from the server to and update the verb menu. + /// + public void AddServerVerbs(Dictionary>? verbs) + { + RootMenu.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"))); + return; + } + + // Add the new server-side verbs. + foreach (var (verbType, verbSet) in verbs) + { + if (!CurrentVerbs.TryAdd(verbType, new SortedSet(verbSet))) + { + CurrentVerbs[verbType].UnionWith(verbSet); + } + } + + FillVerbPopup(); + } + + public override void OnKeyBindDown(ContextMenuElement element, GUIBoundKeyEventArgs args) + { + if (args.Function != EngineKeyFunctions.Use && args.Function != ContentKeyFunctions.ActivateItemInWorld) + return; + + if (element is not VerbMenuElement verbElement) + return; + + var verb = verbElement.Verb; + + if (verb == null) + { + // The user probably clicked on a verb category. + // We will act as if they clicked on the first verb in that category. + + if (verbElement.SubMenu == null || verbElement.SubMenu.ChildCount == 0) + return; + + if (verbElement.SubMenu.MenuBody.Children.First() is not VerbMenuElement verbCategoryElement) + return; + + verb = verbCategoryElement.Verb; + + if (verb == null) + return; + } + + _verbSystem.ExecuteVerb(CurrentTarget, verb, verbElement.Type); + if (verb.CloseMenu) + _verbSystem.CloseAllMenus(); + + args.Handle(); + } + } +} diff --git a/Content.Client/Verbs/VerbMenuElement.cs b/Content.Client/Verbs/VerbMenuElement.cs deleted file mode 100644 index b4681e82ac..0000000000 --- a/Content.Client/Verbs/VerbMenuElement.cs +++ /dev/null @@ -1,246 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using Content.Client.ContextMenu.UI; -using Content.Client.Resources; -using Content.Shared.Verbs; -using Robust.Client.Graphics; -using Robust.Client.ResourceManagement; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Client.Utility; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Maths; -using Robust.Shared.Utility; -using static Robust.Client.UserInterface.Controls.BoxContainer; -using Timer = Robust.Shared.Timing.Timer; - -namespace Content.Client.Verbs -{ - - /// - /// This pop-up appears when hovering over a verb category in the context menu. - /// - public sealed class VerbCategoryPopup : ContextMenuPopup - { - public VerbCategoryPopup(VerbSystem system, IEnumerable verbs, VerbType type, EntityUid target, bool drawOnlyIcons) - : base() - { - // Do any verbs have icons? If not, don't bother leaving space for icons in the pop-up. - var drawVerbIcons = false; - foreach (var verb in verbs) - { - if (verb.Icon != null) - { - drawVerbIcons = true; - break; - } - } - - // If no verbs have icons. we cannot draw only icons - if (drawVerbIcons == false) - drawOnlyIcons = false; - - // If we are drawing only icons, show them side by side - if (drawOnlyIcons) - List.Orientation = LayoutOrientation.Horizontal; - - foreach (var verb in verbs) - { - AddToMenu(new VerbButton(system, verb, type, target, drawVerbIcons)); - } - } - } - - public sealed class VerbButton : BaseButton - { - public VerbButton(VerbSystem system, Verb verb, VerbType type, EntityUid target, bool drawIcons = true, bool categoryPrefix = false) : base() - { - Disabled = verb.Disabled; - ToolTip = verb.Tooltip; - TooltipDelay = 0.5f; - - var buttonContents = new BoxContainer { Orientation = LayoutOrientation.Horizontal }; - - // maybe draw verb icons - if (drawIcons) - { - TextureRect icon = new() - { - MinSize = (ContextMenuPopup.ButtonHeight, ContextMenuPopup.ButtonHeight), - Stretch = TextureRect.StretchMode.KeepCentered, - TextureScale = (0.5f, 0.5f) - }; - - // Even though we are drawing icons, the icon for this specific verb may be null. - if (verb.Icon != null) - { - icon.Texture = verb.Icon.Frame0(); - } else if (categoryPrefix && verb.Category?.Icon != null) - { - // we will use the category icon instead - icon.Texture = verb.Category.Icon.Frame0(); - } - - buttonContents.AddChild(icon); - } - - // maybe add a label - if (verb.Text != string.Empty || categoryPrefix) - { - // First add a small bit of padding - buttonContents.AddChild(new Control { MinSize = (4, ContextMenuPopup.ButtonHeight) }); - - var label = new RichTextLabel(); - var text = categoryPrefix ? verb.Category!.Text + " " + verb.Text : verb.Text; - label.SetMessage(FormattedMessage.FromMarkupPermissive(text.Trim())); - label.VerticalAlignment = VAlignment.Center; - buttonContents.AddChild(label); - - // Then also add some padding after the text. - buttonContents.AddChild(new Control { MinSize = (4, ContextMenuPopup.ButtonHeight) }); - } - - AddChild(buttonContents); - - if (Disabled) - return; - - // give the button functionality! - OnPressed += _ => - { - if (verb.CloseMenu) - system.ContextMenuPresenter.CloseAllMenus(); - system.TryExecuteVerb(verb, target, type); - }; - } - - protected override void Draw(DrawingHandleScreen handle) - { - base.Draw(handle); - - if (Disabled) - { - // use transparent-black rectangle to create a darker background. - handle.DrawRect(PixelSizeBox, new Color(0,0,0,155)); - } - else if (DrawMode == DrawModeEnum.Hover) - { - // Draw a lighter shade of gray when hovered over - handle.DrawRect(PixelSizeBox, Color.DarkSlateGray); - } - } - } - - public sealed class VerbCategoryButton : Control - { - private readonly VerbSystem _system; - - private CancellationTokenSource? _openCancel; - - /// - /// Whether or not to hide member verb text and just show icons. - /// - /// - /// If no members have icons, this option is ignored and text is shown anyways. Defaults to using . - /// - private readonly bool _drawOnlyIcons; - - /// - /// The pop-up that appears when hovering over this verb group. - /// - private readonly VerbCategoryPopup _popup; - - public VerbCategoryButton(VerbSystem system, VerbCategory category, IEnumerable verbs, VerbType type, EntityUid target, bool? drawOnlyIcons = null) : base() - { - _system = system; - _drawOnlyIcons = drawOnlyIcons ?? category.IconsOnly; - - MouseFilter = MouseFilterMode.Stop; - - // Contents of the button stored in this box container - var box = new BoxContainer() { Orientation = LayoutOrientation.Horizontal }; - - // First we add the icon for the verb group - var icon = new TextureRect - { - MinSize = (ContextMenuPopup.ButtonHeight, ContextMenuPopup.ButtonHeight), - TextureScale = (0.5f, 0.5f), - Stretch = TextureRect.StretchMode.KeepCentered, - }; - if (category.Icon != null) - { - icon.Texture = category.Icon.Frame0(); - } - box.AddChild(icon); - - // Some padding before the text - box.AddChild(new Control { MinSize = (4, ContextMenuPopup.ButtonHeight) }); - - // Then we add the label - var label = new RichTextLabel(); - label.SetMessage(FormattedMessage.FromMarkupPermissive(category.Text)); - label.HorizontalExpand = true; - label.VerticalAlignment = VAlignment.Center; - box.AddChild(label); - - // Then also add some padding after the text. - box.AddChild(new Control { MinSize = (4, ContextMenuPopup.ButtonHeight) }); - - // Then add the little ">" icon that tells you it's a group of verbs - box.AddChild(new TextureRect - { - Texture = IoCManager.Resolve() - .GetTexture("/Textures/Interface/VerbIcons/group.svg.192dpi.png"), - TextureScale = (0.5f, 0.5f), - Stretch = TextureRect.StretchMode.KeepCentered, - }); - - // The pop-up that appears when hovering over the button - _popup = new VerbCategoryPopup(_system, verbs, type, target, _drawOnlyIcons); - UserInterfaceManager.ModalRoot.AddChild(_popup); - - AddChild(box); - } - - protected override void Draw(DrawingHandleScreen handle) - { - base.Draw(handle); - - if (this == UserInterfaceManager.CurrentlyHovered || - _system.CurrentCategoryPopup == _popup) - { - handle.DrawRect(PixelSizeBox, Color.DarkSlateGray); - } - } - - /// - /// Open a verb category pop-up after a short delay. - /// - protected override void MouseEntered() - { - base.MouseEntered(); - - _openCancel = new CancellationTokenSource(); - - Timer.Spawn(ContextMenuPresenter.HoverDelay, () => - { - _system.CurrentCategoryPopup?.Close(); - _system.CurrentCategoryPopup = _popup; - var upperRight = GlobalPosition + (Width + ContextMenuPopup.MarginSize, -ContextMenuPopup.MarginSize); - _popup.Open(UIBox2.FromDimensions(upperRight, (1, 1)), GlobalPosition); - }, _openCancel.Token); - } - - /// - /// Cancel the delayed pop-up - /// - protected override void MouseExited() - { - base.MouseExited(); - - _openCancel?.Cancel(); - _openCancel = null; - } - } -} diff --git a/Content.Client/Verbs/VerbSystem.cs b/Content.Client/Verbs/VerbSystem.cs index eeace00254..e93f79b73d 100644 --- a/Content.Client/Verbs/VerbSystem.cs +++ b/Content.Client/Verbs/VerbSystem.cs @@ -1,15 +1,22 @@ +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Client.ContextMenu.UI; +using Content.Client.Popups; +using Content.Client.Verbs.UI; +using Content.Shared.Examine; using Content.Shared.GameTicking; +using Content.Shared.Interaction.Helpers; +using Content.Shared.Tag; using Content.Shared.Verbs; using JetBrains.Annotations; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; using Robust.Client.Player; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; +using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.IoC; -using Robust.Shared.Localization; using Robust.Shared.Map; using Robust.Shared.Maths; @@ -18,211 +25,201 @@ namespace Content.Client.Verbs [UsedImplicitly] public sealed class VerbSystem : SharedVerbSystem { - [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly IEntityLookup _entityLookup = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; - public ContextMenuPresenter ContextMenuPresenter = default!; + /// + /// When a user right clicks somewhere, how large is the box we use to get entities for the context menu? + /// + public const float EntityMenuLookupSize = 1f; - public EntityUid CurrentTarget; - public ContextMenuPopup? CurrentVerbPopup; - public ContextMenuPopup? CurrentCategoryPopup; - public Dictionary> CurrentVerbs = new(); + public EntityMenuPresenter EntityMenu = default!; + public VerbMenuPresenter VerbMenu = default!; + + [Dependency] private readonly IEyeManager _eyeManager = default!; /// - /// Whether to show all entities on the context menu. + /// These flags determine what entities the user can see on the context menu. /// - /// - /// Verb execution will only be affected if the server also agrees that this player can see the target - /// entity. - /// - public bool CanSeeAllContext = false; + public MenuVisibility Visibility; - // TODO VERBS Move presenter out of the system - // TODO VERBS Separate the rest of the UI from the logic public override void Initialize() { base.Initialize(); SubscribeNetworkEvent(Reset); SubscribeNetworkEvent(HandleVerbResponse); - SubscribeNetworkEvent(SetSeeAllContext); - ContextMenuPresenter = new ContextMenuPresenter(this); + EntityMenu = new(this); + VerbMenu = new(this); } - private void Reset(RoundRestartCleanupEvent ev) + public void Reset(RoundRestartCleanupEvent ev) { - ContextMenuPresenter.CloseAllMenus(); + CloseAllMenus(); } public override void Shutdown() { base.Shutdown(); - ContextMenuPresenter?.Dispose(); + EntityMenu?.Dispose(); + VerbMenu?.Dispose(); } public override void Update(float frameTime) { base.Update(frameTime); - ContextMenuPresenter?.Update(); + EntityMenu?.Update(); } - private void SetSeeAllContext(SetSeeAllContextEvent args) + public void CloseAllMenus() { - CanSeeAllContext = args.CanSeeAllContext; + EntityMenu.Close(); + VerbMenu.Close(); } /// - /// Execute actions associated with the given verb. If there are no defined actions, this will instead ask - /// the server to run the given verb. + /// Get all of the entities in an area for displaying on the context menu. /// - public void TryExecuteVerb(Verb verb, EntityUid target, VerbType verbType) + public bool TryGetEntityMenuEntities(MapCoordinates targetPos, [NotNullWhen(true)] out List? result) { - if (!TryExecuteVerb(verb)) - RaiseNetworkEvent(new TryExecuteVerbEvent(target, verb, verbType)); - } + result = null; + var player = _playerManager.LocalPlayer?.ControlledEntity; - public void OpenVerbMenu(IEntity target, ScreenCoordinates screenCoordinates) - { - if (CurrentVerbPopup != null) + if (player == null) + return false; + + var visibility = _eyeManager.CurrentEye.DrawFov + ? Visibility + : Visibility | MenuVisibility.NoFov; + + // Check if we have LOS to the clicked-location. + if ((visibility & MenuVisibility.NoFov) == 0 && + !player.InRangeUnOccluded(targetPos, range: ExamineSystemShared.ExamineRange)) + return false; + + // Get entities + var entities = _entityLookup.GetEntitiesIntersecting( + targetPos.MapId, + Box2.CenteredAround(targetPos.Position, (EntityMenuLookupSize, EntityMenuLookupSize))) + .ToList(); + + if (entities.Count == 0) + return false; + + if (visibility == MenuVisibility.All) { - CloseVerbMenu(); + result = entities; + return true; } - var user = _playerManager.LocalPlayer?.ControlledEntity; - if (user == null) - return; + // remove any entities in containers + if ((visibility & MenuVisibility.InContainer) == 0) + { + foreach (var entity in entities.ToList()) + { + if (!player.IsInSameOrTransparentContainer(entity)) + entities.Remove(entity); + } + } - CurrentTarget = target.Uid; + // remove any invisible entities + if ((visibility & MenuVisibility.Invisible) == 0) + { + foreach (var entity in entities.ToList()) + { + if (!EntityManager.TryGetComponent(entity.Uid, out ISpriteComponent? spriteComponent) || + !spriteComponent.Visible) + { + entities.Remove(entity); + continue; + } - CurrentVerbPopup = new ContextMenuPopup(); - _userInterfaceManager.ModalRoot.AddChild(CurrentVerbPopup); - CurrentVerbPopup.OnPopupHide += CloseVerbMenu; + if (entity.HasTag("HideContextMenu")) + entities.Remove(entity); + } + } - CurrentVerbs = GetVerbs(target, user, VerbType.All); + // Remove any entities that do not have LOS + if ((visibility & MenuVisibility.NoFov) == 0) + { + var playerPos = player.Transform.MapPosition; + foreach (var entity in entities.ToList()) + { + if (!ExamineSystemShared.InRangeUnOccluded( + playerPos, + entity.Transform.MapPosition, + ExamineSystemShared.ExamineRange, + null)) + { + entities.Remove(entity); + } + } + } + if (entities.Count == 0) + return false; + + result = entities; + return true; + } + + /// + /// Ask the server to send back a list of server-side verbs, and for now return an incomplete list of verbs + /// (only those defined locally). + /// + public Dictionary> GetVerbs(IEntity target, IEntity user, VerbType verbTypes) + { if (!target.Uid.IsClientSide()) { - CurrentVerbPopup.AddToMenu(new Label { Text = Loc.GetString("verb-system-waiting-on-server-text") }); - RaiseNetworkEvent(new RequestServerVerbsEvent(CurrentTarget, VerbType.All)); + RaiseNetworkEvent(new RequestServerVerbsEvent(target.Uid, verbTypes)); } - - // Show the menu - FillVerbPopup(CurrentVerbPopup); - var box = UIBox2.FromDimensions(screenCoordinates.Position, (1, 1)); - CurrentVerbPopup.Open(box); + + return GetLocalVerbs(target, user, verbTypes); } - public void OnContextButtonPressed(IEntity entity) + /// + /// Execute actions associated with the given verb. + /// + /// + /// Unless this is a client-exclusive verb, this will also tell the server to run the same verb. However, if the verb + /// is disabled and has a tooltip, this function will only generate a pop-up-message instead of executing anything. + /// + public void ExecuteVerb(EntityUid target, Verb verb, VerbType verbType) { - OpenVerbMenu(entity, _userInterfaceManager.MousePositionScaled); + if (verb.Disabled) + { + if (verb.Message != null) + _popupSystem.PopupCursor(verb.Message); + return; + } + + ExecuteVerb(verb); + + if (!verb.ClientExclusive) + { + RaiseNetworkEvent(new ExecuteVerbEvent(target, verb, verbType)); + } } private void HandleVerbResponse(VerbsResponseEvent msg) { - if (CurrentTarget != msg.Entity || CurrentVerbPopup == null) - { - return; - } - - // This **should** not happen. - if (msg.Verbs == null) - { - // update "waiting for server...". - CurrentVerbPopup.List.DisposeAllChildren(); - CurrentVerbPopup.AddToMenu(new Label { Text = Loc.GetString("verb-system-null-server-response") }); - FillVerbPopup(CurrentVerbPopup); - return; - } - - // Add the new server-side verbs. - foreach (var (verbType, verbSet) in msg.Verbs) - { - SortedSet sortedVerbs = new (verbSet); - if (!CurrentVerbs.TryAdd(verbType, sortedVerbs)) - { - CurrentVerbs[verbType].UnionWith(sortedVerbs); - } - } - - // Clear currently shown verbs and show new ones - CurrentVerbPopup.List.DisposeAllChildren(); - FillVerbPopup(CurrentVerbPopup); - } - - private void FillVerbPopup(ContextMenuPopup popup) - { - if (CurrentTarget == EntityUid.Invalid) + if (!VerbMenu.RootMenu.Visible || VerbMenu.CurrentTarget != msg.Entity) return; - // Add verbs to pop-up, grouped by type. Order determined by how types are defined VerbTypes - var types = CurrentVerbs.Keys.ToList(); - types.Sort(); - foreach (var type in types) - { - AddVerbSet(popup, type); - } - - // Were the verb lists empty? - if (popup.List.ChildCount == 0) - { - var panel = new PanelContainer(); - panel.AddChild(new Label { Text = Loc.GetString("verb-system-no-verbs-text") }); - popup.AddChild(panel); - } - - popup.InvalidateMeasure(); - } - - /// - /// Add a list of verbs to a BoxContainer. Iterates over the given verbs list and creates GUI buttons. - /// - private void AddVerbSet(ContextMenuPopup popup, VerbType type) - { - if (!CurrentVerbs.TryGetValue(type, out var verbSet) || verbSet.Count == 0) - return; - - HashSet listedCategories = new(); - - foreach (var verb in verbSet) - { - if (verb.Category == null) - { - // Lone verb without a category. just create a button for it - popup.AddToMenu(new VerbButton(this, verb, type, CurrentTarget)); - continue; - } - - if (listedCategories.Contains(verb.Category.Text)) - { - // This verb was already included in a verb-category button added by a previous verb - continue; - } - - // Get the verbs in the category - var verbsInCategory = verbSet.Where(v => v.Category?.Text == verb.Category.Text); - - popup.AddToMenu( - new VerbCategoryButton(this, verb.Category, verbsInCategory, type, CurrentTarget)); - listedCategories.Add(verb.Category.Text); - continue; - - } - } - - public void CloseVerbMenu() - { - if (CurrentVerbPopup != null) - { - CurrentVerbPopup.OnPopupHide -= CloseVerbMenu; - CurrentVerbPopup.Dispose(); - CurrentVerbPopup = null; - } - - CurrentCategoryPopup?.Dispose(); - CurrentCategoryPopup = null; - CurrentTarget = EntityUid.Invalid; - CurrentVerbs.Clear(); + VerbMenu.AddServerVerbs(msg.Verbs); } } + + [Flags] + public enum MenuVisibility + { + // What entities can a user see on the entity menu? + Default = 0, // They can only see entities in FoV. + NoFov = 1 << 0, // They ignore FoV restrictions + InContainer = 1 << 1, // They can see through containers. + Invisible = 1 << 2, // They can see entities without sprites and the "HideContextMenu" tag is ignored. + All = NoFov | InContainer | Invisible + } } diff --git a/Content.Client/Viewport/GameScreenBase.cs b/Content.Client/Viewport/GameScreenBase.cs index 6d90d9eadb..fecfe4b493 100644 --- a/Content.Client/Viewport/GameScreenBase.cs +++ b/Content.Client/Viewport/GameScreenBase.cs @@ -1,13 +1,15 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Content.Client.Clickable; +using Content.Client.ContextMenu.UI; using Content.Client.Interactable; using Content.Client.Interactable.Components; using Content.Client.State; using Content.Shared; using Content.Shared.CCVar; using Robust.Client.GameObjects; +using Robust.Client.Graphics; using Robust.Client.Input; using Robust.Client.Player; using Robust.Client.State; @@ -38,6 +40,7 @@ namespace Content.Client.Viewport [Dependency] protected readonly IUserInterfaceManager UserInterfaceManager = default!; [Dependency] protected readonly IConfigurationManager ConfigurationManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IEyeManager _eyeManager = default!; private IEventBus _eventBus => _entityManager.EventBus; @@ -62,6 +65,9 @@ namespace Content.Client.Viewport _outlineEnabled = message.Enabled; } + /// + /// Highlight the currently hovered entity. + /// public override void FrameUpdate(FrameEventArgs e) { base.FrameUpdate(e); @@ -71,6 +77,13 @@ namespace Content.Client.Viewport if (localPlayer == null) return; + // TODO InteractionOutlineComponent + // BUG: The logic that gets the renderScale here assumes that the entity is only visible in a single + // viewport. The entity will be highlighted in ALL viewport where it is visible, regardless of which + // viewport is being used to hover over it. If these Viewports have very different render scales, this may + // lead to extremely thick outlines in the other viewports. Fixing this probably requires changing how the + // hover outline works, so that it only highlights the entity in a single viewport. + IEntity? entityToClick = null; var renderScale = 1; if (UserInterfaceManager.CurrentlyHovered is IViewportControl vp) @@ -83,6 +96,15 @@ namespace Content.Client.Viewport renderScale = svp.CurrentRenderScale; } } + else if (UserInterfaceManager.CurrentlyHovered is EntityMenuElement element) + { + entityToClick = element.Entity; + // TODO InteractionOutlineComponent + // Currently we just take the renderscale from the main viewport. In the future, when the bug mentioned + // above is fixed, the viewport should probably be the one that was clicked on to open the entity menu + // in the first place. + renderScale = _eyeManager.MainViewport.GetRenderScale(); + } var inRange = false; if (localPlayer.ControlledEntity != null && entityToClick != null) diff --git a/Content.Server/Administration/AdminVerbSystem.cs b/Content.Server/Administration/AdminVerbSystem.cs index d78140cc2f..598485b27b 100644 --- a/Content.Server/Administration/AdminVerbSystem.cs +++ b/Content.Server/Administration/AdminVerbSystem.cs @@ -57,7 +57,7 @@ namespace Content.Server.Administration Verb verb = new(); verb.Text = Loc.GetString("delete-verb-get-data-text"); verb.Category = VerbCategory.Debug; - verb.IconTexture = "/Textures/Interface/VerbIcons/delete.svg.192dpi.png"; + verb.IconTexture = "/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png"; verb.Act = () => args.Target.Delete(); args.Verbs.Add(verb); } @@ -200,7 +200,7 @@ namespace Content.Server.Administration // TODO CHEMISTRY // Add reagent ui broke after solution refactor. Needs fixing verb.Disabled = true; - verb.Tooltip = "Currently non functional after solution refactor."; + verb.Message = "Currently non functional after solution refactor."; verb.Priority = -2; args.Verbs.Add(verb); diff --git a/Content.Server/Buckle/Systems/BuckleSystem.cs b/Content.Server/Buckle/Systems/BuckleSystem.cs index 7e2ccac8ee..4337bdd9ad 100644 --- a/Content.Server/Buckle/Systems/BuckleSystem.cs +++ b/Content.Server/Buckle/Systems/BuckleSystem.cs @@ -43,7 +43,8 @@ namespace Content.Server.Buckle.Systems Verb verb = new(); verb.Act = () => component.TryUnbuckle(args.User); - verb.Category = VerbCategory.Unbuckle; + verb.Text = Loc.GetString("verb-categories-unbuckle"); + verb.IconTexture = "/Textures/Interface/VerbIcons/unbuckle.svg.192dpi.png"; if (args.Target == args.User && args.Using == null) { diff --git a/Content.Server/Cabinet/ItemCabinetSystem.cs b/Content.Server/Cabinet/ItemCabinetSystem.cs index de563c0c04..8bc4e23717 100644 --- a/Content.Server/Cabinet/ItemCabinetSystem.cs +++ b/Content.Server/Cabinet/ItemCabinetSystem.cs @@ -64,12 +64,12 @@ namespace Content.Server.Cabinet toggleVerb.Act = () => ToggleItemCabinet(uid, cabinet); if (cabinet.Opened) { - toggleVerb.Text = Loc.GetString("verb-categories-close"); + toggleVerb.Text = Loc.GetString("verb-common-close"); toggleVerb.IconTexture = "/Textures/Interface/VerbIcons/close.svg.192dpi.png"; } else { - toggleVerb.Text = Loc.GetString("verb-categories-open"); + toggleVerb.Text = Loc.GetString("verb-common-open"); toggleVerb.IconTexture = "/Textures/Interface/VerbIcons/open.svg.192dpi.png"; } args.Verbs.Add(toggleVerb); diff --git a/Content.Server/Light/EntitySystems/HandHeldLightSystem.cs b/Content.Server/Light/EntitySystems/HandHeldLightSystem.cs index 8ddb8fec4e..a044d7a59a 100644 --- a/Content.Server/Light/EntitySystems/HandHeldLightSystem.cs +++ b/Content.Server/Light/EntitySystems/HandHeldLightSystem.cs @@ -54,7 +54,7 @@ namespace Content.Server.Light.EntitySystems return; Verb verb = new(); - verb.Text = Loc.GetString("verb-toggle-light"); + verb.Text = Loc.GetString("verb-common-toggle-light"); verb.IconTexture = "/Textures/Interface/VerbIcons/light.svg.192dpi.png"; verb.Act = component.Activated ? () => component.TurnOff() diff --git a/Content.Server/Storage/EntitySystems/StorageSystem.cs b/Content.Server/Storage/EntitySystems/StorageSystem.cs index eb640e3115..b87aceb218 100644 --- a/Content.Server/Storage/EntitySystems/StorageSystem.cs +++ b/Content.Server/Storage/EntitySystems/StorageSystem.cs @@ -70,12 +70,12 @@ namespace Content.Server.Storage.EntitySystems Verb verb = new(); if (component.Open) { - verb.Text = Loc.GetString("verb-categories-close"); + verb.Text = Loc.GetString("verb-common-close"); verb.IconTexture = "/Textures/Interface/VerbIcons/close.svg.192dpi.png"; } else { - verb.Text = Loc.GetString("verb-categories-open"); + verb.Text = Loc.GetString("verb-common-open"); verb.IconTexture = "/Textures/Interface/VerbIcons/open.svg.192dpi.png"; } verb.Act = () => component.ToggleOpen(args.User); @@ -102,12 +102,12 @@ namespace Content.Server.Storage.EntitySystems verb.Act = () => component.OpenStorageUI(args.User); if (uiOpen) { - verb.Text = Loc.GetString("verb-categories-close"); + verb.Text = Loc.GetString("verb-common-close-ui"); verb.IconTexture = "/Textures/Interface/VerbIcons/close.svg.192dpi.png"; } else { - verb.Text = Loc.GetString("verb-categories-open"); + verb.Text = Loc.GetString("verb-common-open-ui"); verb.IconTexture = "/Textures/Interface/VerbIcons/open.svg.192dpi.png"; } args.Verbs.Add(verb); diff --git a/Content.Server/Verbs/ToggleAllContextCommand.cs b/Content.Server/Verbs/ToggleAllContextCommand.cs deleted file mode 100644 index 630e7dda46..0000000000 --- a/Content.Server/Verbs/ToggleAllContextCommand.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Content.Server.Administration; -using Content.Server.Verbs; -using Content.Shared.Administration; -using Robust.Server.Player; -using Robust.Shared.Console; -using Robust.Shared.GameObjects; - -namespace Content.Server.Containers.Commands -{ - [AdminCommand(AdminFlags.Debug)] - public class ToggleAllContextCommand : IConsoleCommand - { - public const string CommandName = "toggleallcontext"; - - // ReSharper disable once StringLiteralTypo - public string Command => CommandName; - public string Description => "Toggles showing all entities visible on the context menu, even when they shouldn't be."; - public string Help => $"{Command}"; - - public void Execute(IConsoleShell shell, string argStr, string[] args) - { - var player = shell.Player as IPlayerSession; - if (player == null) - { - shell.WriteLine("You need to be a player to use this command."); - return; - } - - EntitySystem.Get().ToggleSeeAllContext(player); - } - } -} diff --git a/Content.Server/Verbs/VerbSystem.cs b/Content.Server/Verbs/VerbSystem.cs index 3286e04fbd..ddc5d8d2b0 100644 --- a/Content.Server/Verbs/VerbSystem.cs +++ b/Content.Server/Verbs/VerbSystem.cs @@ -1,8 +1,5 @@ -using System.Collections.Generic; -using Content.Shared.GameTicking; using Content.Shared.Verbs; using Robust.Server.Player; -using Robust.Shared.Enums; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; @@ -13,58 +10,18 @@ namespace Content.Server.Verbs { [Dependency] private readonly IPlayerManager _playerManager = default!; - /// - /// List of players that can see all entities on the context menu, ignoring normal visibility rules. - /// - public readonly HashSet SeeAllContextPlayers = new(); - public override void Initialize() { base.Initialize(); - IoCManager.InjectDependencies(this); - - SubscribeLocalEvent(Reset); SubscribeNetworkEvent(HandleVerbRequest); - SubscribeNetworkEvent(HandleTryExecuteVerb); - - _playerManager.PlayerStatusChanged += PlayerStatusChanged; - } - - public override void Shutdown() - { - base.Shutdown(); - _playerManager.PlayerStatusChanged -= PlayerStatusChanged; - } - - private void PlayerStatusChanged(object? sender, SessionStatusEventArgs args) - { - if (args.NewStatus == SessionStatus.Disconnected) - { - SeeAllContextPlayers.Remove(args.Session); - } - } - - public void Reset(RoundRestartCleanupEvent ev) - { - SeeAllContextPlayers.Clear(); - } - - public void ToggleSeeAllContext(IPlayerSession player) - { - if (!SeeAllContextPlayers.Add(player)) - { - SeeAllContextPlayers.Remove(player); - } - - SetSeeAllContextEvent args = new() { CanSeeAllContext = SeeAllContextPlayers.Contains(player) }; - RaiseNetworkEvent(args, player.ConnectedClient); + SubscribeNetworkEvent(HandleTryExecuteVerb); } /// /// Called when asked over the network to run a given verb. /// - public void HandleTryExecuteVerb(TryExecuteVerbEvent args, EntitySessionEventArgs eventArgs) + public void HandleTryExecuteVerb(ExecuteVerbEvent args, EntitySessionEventArgs eventArgs) { var session = eventArgs.SenderSession; var userEntity = session.AttachedEntity; @@ -81,18 +38,20 @@ namespace Content.Server.Verbs } // Get the list of verbs. This effectively also checks that the requested verb is in fact a valid verb that - // the user can perform. In principle, this might waste time checking & preparing unrelated verbs even - // though we know precisely which one we want. However, MOST entities will only have 1 or 2 verbs of a given - // type. The one exception here is the "other" verb type, which has 3-4 verbs + all the debug verbs. So maybe - // the debug verbs should be made a separate type? - var verbs = GetVerbs(targetEntity, userEntity, args.Type)[args.Type]; + // the user can perform. + var verbs = GetLocalVerbs(targetEntity, userEntity, args.Type)[args.Type]; + + // Note that GetLocalVerbs might waste time checking & preparing unrelated verbs even though we know + // precisely which one we want to run. However, MOST entities will only have 1 or 2 verbs of a given type. + // The one exception here is the "other" verb type, which has 3-4 verbs + all the debug verbs. // Find the requested verb. if (verbs.TryGetValue(args.RequestedVerb, out var verb)) - TryExecuteVerb(verb); + ExecuteVerb(verb); else - // 404 Verb not found - Logger.Warning($"{nameof(HandleTryExecuteVerb)} called by player {session} with an invalid verb: {args.RequestedVerb.Category?.Text} {args.RequestedVerb.Text}"); + // 404 Verb not found. Note that this could happen due to something as simple as opening the verb menu, walking away, then trying + // to run the pickup-item verb. So maybe this shouldn't even be logged? + Logger.Info($"{nameof(HandleTryExecuteVerb)} called by player {session} with an invalid verb: {args.RequestedVerb.Category?.Text} {args.RequestedVerb.Text}"); } private void HandleVerbRequest(RequestServerVerbsEvent args, EntitySessionEventArgs eventArgs) @@ -105,32 +64,17 @@ namespace Content.Server.Verbs return; } - var user = player.AttachedEntity; - - if (user == null) + if (player.AttachedEntity == null) { Logger.Warning($"{nameof(HandleVerbRequest)} called by player {player} with no attached entity."); return; } - // Validate input (check that the user can see the entity) - TryGetContextEntities(user, - target.Transform.MapPosition, - out var entities, - buffer: true, - ignoreVisibility: SeeAllContextPlayers.Contains(player)); - - VerbsResponseEvent response; - if (entities != null && entities.Contains(target)) - { - response = new(args.EntityUid, GetVerbs(target, user, args.Type)); - } - else - { - // Don't leave the client hanging on "Waiting for server....", send empty response. - response = new(args.EntityUid, null); - } + // We do not verify that the user has access to the requested entity. The individual verbs should check + // this, and some verbs (e.g. view variables) won't even care about whether an entity is accessible through + // the entity menu or not. + var response = new VerbsResponseEvent(args.EntityUid, GetLocalVerbs(target, player.AttachedEntity, args.Type)); RaiseNetworkEvent(response, player.ConnectedClient); } } diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 309761dfde..63741cab06 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -404,9 +404,9 @@ namespace Content.Shared.CCVar CVarDef.Create("ooc.enabled_admin", true, CVar.NOTIFY); /* - * Context Menu Grouping Types + * Entity Menu Grouping Types */ - public static readonly CVarDef ContextMenuGroupingType = CVarDef.Create("context_menu", 0, CVar.CLIENTONLY); + public static readonly CVarDef EntityMenuGroupingType = CVarDef.Create("entity_menu", 0, CVar.CLIENTONLY); /* * VOTE diff --git a/Content.Shared/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs index a07a4f24b1..e4e7efef21 100644 --- a/Content.Shared/Interaction/SharedInteractionSystem.cs +++ b/Content.Shared/Interaction/SharedInteractionSystem.cs @@ -530,7 +530,7 @@ namespace Content.Shared.Interaction if (verb.Disabled) continue; - _verbSystem.TryExecuteVerb(verb); + _verbSystem.ExecuteVerb(verb); break; } } diff --git a/Content.Server/Items/ItemSystem.cs b/Content.Shared/Item/ItemSystem.cs similarity index 94% rename from Content.Server/Items/ItemSystem.cs rename to Content.Shared/Item/ItemSystem.cs index 2f03fb5338..8d88b30d63 100644 --- a/Content.Server/Items/ItemSystem.cs +++ b/Content.Shared/Item/ItemSystem.cs @@ -1,10 +1,9 @@ -using Content.Shared.Item; using Content.Shared.Verbs; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.Localization; -namespace Content.Server.Items +namespace Content.Shared.Item { public class ItemSystem : EntitySystem { @@ -29,7 +28,7 @@ namespace Content.Server.Items // if the item already in the user's inventory, change the text if (args.Target.TryGetContainer(out var container) && container.Owner == args.User) - verb.Text = Loc.GetString("pick-up-verb-get-data-text-inventory"); + verb.Text = Loc.GetString("pick-up-verb-get-data-text-inventory"); else verb.Text = Loc.GetString("pick-up-verb-get-data-text"); diff --git a/Content.Shared/Verbs/SharedVerbSystem.cs b/Content.Shared/Verbs/SharedVerbSystem.cs index 10269e1bb7..8468eb0930 100644 --- a/Content.Shared/Verbs/SharedVerbSystem.cs +++ b/Content.Shared/Verbs/SharedVerbSystem.cs @@ -1,79 +1,15 @@ using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Content.Shared.Examine; -using Content.Shared.Interaction.Helpers; -using Content.Shared.Tag; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Map; -using Robust.Shared.Maths; namespace Content.Shared.Verbs { public class SharedVerbSystem : EntitySystem { - [Dependency] private readonly IEntityLookup _lookup = default!; - /// - /// Get all of the entities in an area for displaying on the context menu. + /// Raises a number of events in order to get all verbs of the given type(s) defined in local systems. This + /// does not request verbs from the server. /// - /// Whether we should slightly extend the entity search area. - public bool TryGetContextEntities(IEntity player, MapCoordinates targetPos, - [NotNullWhen(true)] out List? contextEntities, bool buffer = false, bool ignoreVisibility = false) - { - contextEntities = null; - - // Check if we have LOS to the clicked-location. - if (!ignoreVisibility && !player.InRangeUnOccluded(targetPos, range: ExamineSystemShared.ExamineRange)) - return false; - - // Get entities - var length = buffer ? 1.0f : 0.5f; - var entities = _lookup.GetEntitiesIntersecting( - targetPos.MapId, - Box2.CenteredAround(targetPos.Position, (length, length))) - .ToList(); - - if (entities.Count == 0) return false; - - if (ignoreVisibility) - { - contextEntities = entities; - return true; - } - - // perform visibility checks - var playerPos = player.Transform.MapPosition; - foreach (var entity in entities.ToList()) - { - if (entity.HasTag("HideContextMenu")) - { - entities.Remove(entity); - continue; - } - - if (!ExamineSystemShared.InRangeUnOccluded( - playerPos, - entity.Transform.MapPosition, - ExamineSystemShared.ExamineRange, - null) ) - { - entities.Remove(entity); - } - } - - if (entities.Count == 0) - return false; - - contextEntities = entities; - return true; - } - - /// - /// Raises a number of events in order to get all verbs of the given type(s) - /// - public Dictionary> GetVerbs(IEntity target, IEntity user, VerbType verbTypes) + public virtual Dictionary> GetLocalVerbs(IEntity target, IEntity user, VerbType verbTypes) { Dictionary> verbs = new(); @@ -109,41 +45,24 @@ namespace Content.Shared.Verbs } /// - /// Execute actions associated with the given verb. + /// Execute the provided verb. /// /// - /// This will try to call delegates and raise any events for the given verb. + /// This will try to call the action delegates and raise the local events for the given verb. /// - public bool TryExecuteVerb(Verb verb) + public void ExecuteVerb(Verb verb) { - var executed = false; - // Maybe run a delegate - if (verb.Act != null) - { - executed = true; - verb.Act.Invoke(); - } - + verb.Act?.Invoke(); + // Maybe raise a local event - if (verb.LocalVerbEventArgs != null) + if (verb.ExecutionEventArgs != null) { - executed = true; - if (verb.LocalEventTarget.IsValid()) - RaiseLocalEvent(verb.LocalEventTarget, verb.LocalVerbEventArgs); + if (verb.EventTarget.IsValid()) + RaiseLocalEvent(verb.EventTarget, verb.ExecutionEventArgs); else - RaiseLocalEvent(verb.LocalVerbEventArgs); + RaiseLocalEvent(verb.ExecutionEventArgs); } - - // maybe raise a network event - if (verb.NetworkVerbEventArgs != null) - { - executed = true; - RaiseNetworkEvent(verb.NetworkVerbEventArgs); - } - - // return false if all of these were null - return executed; } } } diff --git a/Content.Shared/Verbs/Verb.cs b/Content.Shared/Verbs/Verb.cs index 7660ea38d0..72469a8c53 100644 --- a/Content.Shared/Verbs/Verb.cs +++ b/Content.Shared/Verbs/Verb.cs @@ -28,38 +28,38 @@ namespace Content.Shared.Verbs /// /// /// This delegate probably just points to some function in the system assembling this verb. This delegate - /// will be run regardless of whether or - /// are defined. + /// will be run regardless of whether is defined. /// [NonSerialized] public Action? Act; /// - /// This is local event that will be raised when the verb is executed. + /// This is a general local event that will be raised when the verb is executed. /// /// - /// This event will be raised regardless of whether or - /// are defined. + /// If not null, this event will be raised regardless of whether was run. If this event + /// exists purely to call a specific system method, then should probably be used instead (method + /// events are a no-go). /// [NonSerialized] - public object? LocalVerbEventArgs; + public object? ExecutionEventArgs; /// - /// Where do direct the local event. + /// Where do direct the local event. If invalid, the event is not raised directed at any entity. /// [NonSerialized] - public EntityUid LocalEventTarget = EntityUid.Invalid; + public EntityUid EventTarget = EntityUid.Invalid; /// - /// This is networked event that will be raised when the verb is executed. + /// If a verb is only defined client-side, this should be set to true. /// /// - /// This event will be raised regardless of whether or - /// are defined. + /// If true, the client will not also ask the server to run this verb when executed locally. This just + /// prevents unnecessary network events and "404-verb-not-found" log entries. /// [NonSerialized] - public EntityEventArgs? NetworkVerbEventArgs; - + public bool ClientExclusive; + /// /// The text that the user sees on the verb button. /// @@ -74,6 +74,7 @@ namespace Content.Shared.Verbs IconTexture == null ? null : new SpriteSpecifier.Texture(new ResourcePath(IconTexture)); set => _icon = value; } + [NonSerialized] private SpriteSpecifier? _icon; /// @@ -86,18 +87,20 @@ namespace Content.Shared.Verbs /// /// /// Disabled verbs are shown in the context menu with a slightly darker background color, and cannot be - /// executed. It is recommended that a message be provided outlining why this verb is + /// executed. It is recommended that a message be provided outlining why this verb is /// disabled. /// public bool Disabled; /// - /// Optional tooltip to show when hovering over this verb. + /// Optional informative message. /// /// - /// Useful for disabled verbs as a replacement for informative pop-up messages. + /// This will be shown as a tooltip when hovering over this verb in the context menu. Additionally, iF a + /// verb is executed, this message will also be shown as a pop-up message. Useful for + /// disabled verbs to inform users about why they cannot perform a given action. /// - public string? Tooltip; + public string? Message; /// /// Determines the priority of the verb. This affects both how the verb is displayed in the context menu @@ -109,7 +112,7 @@ namespace Content.Shared.Verbs public int Priority; /// - /// Raw texture path used to load the . + /// Raw texture path used to load the for displaying on the client. /// public string? IconTexture; diff --git a/Content.Shared/Verbs/VerbCategory.cs b/Content.Shared/Verbs/VerbCategory.cs index f06b84f5e9..b87d0ca5ac 100644 --- a/Content.Shared/Verbs/VerbCategory.cs +++ b/Content.Shared/Verbs/VerbCategory.cs @@ -16,7 +16,8 @@ namespace Content.Shared.Verbs public readonly SpriteSpecifier? Icon; /// - /// If true, this verb category is shown in the context menu as a row of icons without any text. + /// If true, the members of this verb category will be shown in the context menu as a row of icons without + /// any text. /// /// /// For example, the 'Rotate' category simply shows two icons for rotating left and right. diff --git a/Content.Shared/Verbs/VerbEvents.cs b/Content.Shared/Verbs/VerbEvents.cs index 4b1175aace..974614f3e2 100644 --- a/Content.Shared/Verbs/VerbEvents.cs +++ b/Content.Shared/Verbs/VerbEvents.cs @@ -46,7 +46,7 @@ namespace Content.Shared.Verbs } [Serializable, NetSerializable] - public class TryExecuteVerbEvent : EntityEventArgs + public class ExecuteVerbEvent : EntityEventArgs { public readonly EntityUid Target; public readonly Verb RequestedVerb; @@ -56,7 +56,7 @@ namespace Content.Shared.Verbs /// public readonly VerbType Type; - public TryExecuteVerbEvent(EntityUid target, Verb requestedVerb, VerbType type) + public ExecuteVerbEvent(EntityUid target, Verb requestedVerb, VerbType type) { Target = target; RequestedVerb = requestedVerb; @@ -64,15 +64,6 @@ namespace Content.Shared.Verbs } } - /// - /// Event used to toggle visibility of all context menu entities. - /// - [Serializable, NetSerializable] - public class SetSeeAllContextEvent : EntityEventArgs - { - public bool CanSeeAllContext = false; - } - /// /// Request primary interaction verbs. This includes both use-in-hand and interacting with external entities. /// diff --git a/Resources/Locale/en-US/verbs/verb-system.ftl b/Resources/Locale/en-US/verbs/verb-system.ftl index 5bdb607ffa..5f2251e03a 100644 --- a/Resources/Locale/en-US/verbs/verb-system.ftl +++ b/Resources/Locale/en-US/verbs/verb-system.ftl @@ -1,5 +1,4 @@ verb-system-waiting-on-server-text = Waiting on Server... -verb-system-no-verbs-text = No verbs! verb-system-null-server-response = Entity not in view. You should not see this. @@ -10,15 +9,17 @@ verb-self-target-pronoun = Yourself # verb categories & common verbs. These appear across multiple systems, so they may as well go here. -verb-toggle-light = Toggle light - verb-categories-debug = Debug verb-categories-eject = Eject verb-categories-insert = Insert verb-categories-buckle = Buckle verb-categories-unbuckle = Unbuckle -verb-categories-close = Close -verb-categories-open = Open verb-categories-rotate = Rotate verb-categories-transfer = Set Transfer Amount verb-categories-split = Split + +verb-common-toggle-light = Toggle light +verb-common-close = Close +verb-common-open = Open +verb-common-close-ui = Close UI +verb-common-open-ui = Open UI