diff --git a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs index 31967f956c..9429f05104 100644 --- a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs +++ b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs @@ -2,10 +2,12 @@ using System.Linq; using Content.Client.Administration.Systems; using Content.Client.UserInterface.Controls; using Content.Client.Verbs; +using Content.Client.Verbs.UI; using Content.Shared.Administration; using Content.Shared.Input; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; +using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Input; @@ -56,7 +58,7 @@ namespace Content.Client.Administration.UI.CustomControls } else if (args.Event.Function == EngineKeyFunctions.UseSecondary && selectedPlayer.EntityUid != null) { - _verbSystem.VerbMenu.OpenVerbMenu(selectedPlayer.EntityUid.Value); + IoCManager.Resolve().GetUIController().OpenVerbMenu(selectedPlayer.EntityUid.Value); } } diff --git a/Content.Client/CombatMode/CombatModeComponent.cs b/Content.Client/CombatMode/CombatModeComponent.cs index 482a704adf..441a9ebcfc 100644 --- a/Content.Client/CombatMode/CombatModeComponent.cs +++ b/Content.Client/CombatMode/CombatModeComponent.cs @@ -1,7 +1,9 @@ +using Content.Client.ContextMenu.UI; using Content.Client.Verbs; using Content.Shared.CombatMode; using Content.Shared.Targeting; using Robust.Client.Player; +using Robust.Client.UserInterface; namespace Content.Client.CombatMode { @@ -38,8 +40,7 @@ namespace Content.Client.CombatMode return; } - var verbs = IoCManager.Resolve().GetEntitySystem(); - verbs.CloseAllMenus(); + IoCManager.Resolve().GetUIController().Close(); } } } diff --git a/Content.Client/Commands/GroupingEntityMenuCommand.cs b/Content.Client/Commands/GroupingEntityMenuCommand.cs index f64c5c6332..92315735ec 100644 --- a/Content.Client/Commands/GroupingEntityMenuCommand.cs +++ b/Content.Client/Commands/GroupingEntityMenuCommand.cs @@ -12,7 +12,7 @@ namespace Content.Client.Commands public string Description => "Sets the entity menu grouping type."; - public string Help => $"Usage: entitymenug <0:{EntityMenuPresenter.GroupingTypesCount}>"; + public string Help => $"Usage: entitymenug <0:{EntityMenuUIController.GroupingTypesCount}>"; public void Execute(IConsoleShell shell, string argStr, string[] args) { if (args.Length != 1) @@ -27,7 +27,7 @@ namespace Content.Client.Commands return; } - if (id < 0 ||id > EntityMenuPresenter.GroupingTypesCount - 1) + if (id < 0 ||id > EntityMenuUIController.GroupingTypesCount - 1) { shell.WriteLine($"{args[0]} is not a valid integer."); return; diff --git a/Content.Client/ContextMenu/UI/ContextMenuPopup.xaml.cs b/Content.Client/ContextMenu/UI/ContextMenuPopup.xaml.cs index 05b25757a3..90836ade4c 100644 --- a/Content.Client/ContextMenu/UI/ContextMenuPopup.xaml.cs +++ b/Content.Client/ContextMenu/UI/ContextMenuPopup.xaml.cs @@ -31,14 +31,14 @@ namespace Content.Client.ContextMenu.UI /// public GridContainer MenuBody = new(); - private ContextMenuPresenter _presenter; + private ContextMenuUIController _uiController; - public ContextMenuPopup (ContextMenuPresenter presenter, ContextMenuElement? parentElement) : base() + public ContextMenuPopup (ContextMenuUIController uiController, ContextMenuElement? parentElement) : base() { RobustXamlLoader.Load(this); MenuPanel.SetOnlyStyleClass(StyleClassContextMenuPopup); - _presenter = presenter; + _uiController = uiController; ParentElement = parentElement; // TODO xaml controls now have the access options -> re-xamlify all this. @@ -52,7 +52,7 @@ namespace Content.Client.ContextMenu.UI MenuPanel.MaxHeight = MaxItemsBeforeScroll * (ContextMenuElement.ElementHeight + 2 * ContextMenuElement.ElementMargin) + styleSize.Y; UserInterfaceManager.ModalRoot.AddChild(this); - MenuBody.OnChildRemoved += ctrl => _presenter.OnRemoveElement(this, ctrl); + MenuBody.OnChildRemoved += ctrl => _uiController.OnRemoveElement(this, ctrl); MenuBody.VSeparationOverride = 0; MenuBody.HSeparationOverride = 0; @@ -67,13 +67,13 @@ namespace Content.Client.ContextMenu.UI OnPopupHide += () => { if (ParentElement != null) - _presenter.CloseSubMenus(ParentElement.ParentMenu); + _uiController.CloseSubMenus(ParentElement.ParentMenu); }; } protected override void Dispose(bool disposing) { - MenuBody.OnChildRemoved -= ctrl => _presenter.OnRemoveElement(this, ctrl); + MenuBody.OnChildRemoved -= ctrl => _uiController.OnRemoveElement(this, ctrl); ParentElement = null; base.Dispose(disposing); } diff --git a/Content.Client/ContextMenu/UI/ContextMenuPresenter.cs b/Content.Client/ContextMenu/UI/ContextMenuUIController.cs similarity index 78% rename from Content.Client/ContextMenu/UI/ContextMenuPresenter.cs rename to Content.Client/ContextMenu/UI/ContextMenuUIController.cs index a19f225214..2569cef14b 100644 --- a/Content.Client/ContextMenu/UI/ContextMenuPresenter.cs +++ b/Content.Client/ContextMenu/UI/ContextMenuUIController.cs @@ -1,24 +1,26 @@ -using System; -using System.Collections.Generic; using System.Threading; +using Content.Client.Gameplay; using Robust.Client.UserInterface; -using Robust.Shared.Log; -using Robust.Shared.Maths; +using Robust.Client.UserInterface.Controllers; using Timer = Robust.Shared.Timing.Timer; namespace Content.Client.ContextMenu.UI { /// - /// This class handles all the logic associated with showing a context menu. + /// This class handles all the logic associated with showing a context menu, as well as all the state for the + /// entire context menu stack, including verb and entity menus. It does not currently support multiple + /// open context menus. /// /// /// This largely involves setting up timers to open and close sub-menus when hovering over other menu elements. /// - [Virtual] - public class ContextMenuPresenter : IDisposable + public sealed class ContextMenuUIController : UIController, IOnStateEntered, IOnStateExited { public static readonly TimeSpan HoverDelay = TimeSpan.FromSeconds(0.2); - public ContextMenuPopup RootMenu; + /// + /// Root menu of the entire context menu. + /// + public ContextMenuPopup RootMenu = default!; public Stack Menus { get; } = new(); /// @@ -31,30 +33,35 @@ namespace Content.Client.ContextMenu.UI /// public CancellationTokenSource? CancelClose; - public ContextMenuPresenter() + public Action? OnContextClosed; + public Action? OnContextMouseEntered; + public Action? OnContextMouseExited; + public Action? OnSubMenuOpened; + public Action? OnContextKeyEvent; + + public void OnStateEntered(GameplayState state) { RootMenu = new(this, null); - RootMenu.OnPopupHide += RootMenu.MenuBody.DisposeAllChildren; + RootMenu.OnPopupHide += Close; Menus.Push(RootMenu); } - /// - /// Dispose of all UI elements. - /// - public virtual void Dispose() + public void OnStateExited(GameplayState state) { - RootMenu.OnPopupHide -= RootMenu.MenuBody.DisposeAllChildren; + Close(); + RootMenu.OnPopupHide -= Close; RootMenu.Dispose(); } /// /// Close and clear the root menu. This will also dispose any sub-menus. /// - public virtual void Close() + public void Close() { - RootMenu.Close(); + RootMenu.MenuBody.DisposeAllChildren(); CancelOpen?.Cancel(); CancelClose?.Cancel(); + OnContextClosed?.Invoke(); } /// @@ -82,7 +89,7 @@ namespace Content.Client.ContextMenu.UI /// /// Start a timer to open this element's sub-menu. /// - public virtual void OnMouseEntered(ContextMenuElement element) + private void OnMouseEntered(ContextMenuElement element) { if (!Menus.TryPeek(out var topMenu)) { @@ -100,6 +107,7 @@ namespace Content.Client.ContextMenu.UI CancelOpen?.Cancel(); CancelOpen = new(); Timer.Spawn(HoverDelay, () => OpenSubMenu(element), CancelOpen.Token); + OnContextMouseEntered?.Invoke(element); } /// @@ -108,7 +116,7 @@ namespace Content.Client.ContextMenu.UI /// /// Note that this timer will be aborted when entering the actual sub-menu itself. /// - public virtual void OnMouseExited(ContextMenuElement element) + private void OnMouseExited(ContextMenuElement element) { CancelOpen?.Cancel(); @@ -118,9 +126,13 @@ namespace Content.Client.ContextMenu.UI CancelClose?.Cancel(); CancelClose = new(); Timer.Spawn(HoverDelay, () => CloseSubMenus(element.ParentMenu), CancelClose.Token); + OnContextMouseExited?.Invoke(element); } - public virtual void OnKeyBindDown(ContextMenuElement element, GUIBoundKeyEventArgs args) { } + private void OnKeyBindDown(ContextMenuElement element, GUIBoundKeyEventArgs args) + { + OnContextKeyEvent?.Invoke(element, args); + } /// /// Opens a new sub menu, and close the old one. @@ -128,7 +140,7 @@ namespace Content.Client.ContextMenu.UI /// /// If the given element has no sub-menu, just close the current one. /// - public virtual void OpenSubMenu(ContextMenuElement element) + public void OpenSubMenu(ContextMenuElement element) { if (!Menus.TryPeek(out var topMenu)) { @@ -164,6 +176,7 @@ namespace Content.Client.ContextMenu.UI element.SubMenu.SetPositionLast(); Menus.Push(element.SubMenu); + OnSubMenuOpened?.Invoke(element); } /// diff --git a/Content.Client/ContextMenu/UI/EntityMenuElement.cs b/Content.Client/ContextMenu/UI/EntityMenuElement.cs index c371418c07..11c9f94089 100644 --- a/Content.Client/ContextMenu/UI/EntityMenuElement.cs +++ b/Content.Client/ContextMenu/UI/EntityMenuElement.cs @@ -45,12 +45,12 @@ namespace Content.Client.ContextMenu.UI LayoutContainer.SetGrowVertical(CountLabel, LayoutContainer.GrowDirection.Begin); Entity = entity; - if (Entity != null) - { - Count = 1; - CountLabel.Visible = false; - UpdateEntity(); - } + if (Entity == null) + return; + + Count = 1; + CountLabel.Visible = false; + UpdateEntity(); } protected override void Dispose(bool disposing) diff --git a/Content.Client/ContextMenu/UI/EntityMenuPresenterGrouping.cs b/Content.Client/ContextMenu/UI/EntityMenuPresenterGrouping.cs index 56f5f321e6..231f7f13db 100644 --- a/Content.Client/ContextMenu/UI/EntityMenuPresenterGrouping.cs +++ b/Content.Client/ContextMenu/UI/EntityMenuPresenterGrouping.cs @@ -1,16 +1,17 @@ using Content.Shared.IdentityManagement; using Robust.Client.GameObjects; using System.Linq; +using Robust.Client.UserInterface.Controllers; namespace Content.Client.ContextMenu.UI { - public sealed partial class EntityMenuPresenter : ContextMenuPresenter + public sealed partial class EntityMenuUIController { public const int GroupingTypesCount = 2; private int GroupingContextMenuType { get; set; } public void OnGroupingChanged(int obj) { - Close(); + _context.Close(); GroupingContextMenuType = obj; } diff --git a/Content.Client/ContextMenu/UI/EntityMenuPresenter.cs b/Content.Client/ContextMenu/UI/EntityMenuUIController.cs similarity index 80% rename from Content.Client/ContextMenu/UI/EntityMenuPresenter.cs rename to Content.Client/ContextMenu/UI/EntityMenuUIController.cs index 3d9577d397..9754eed84d 100644 --- a/Content.Client/ContextMenu/UI/EntityMenuPresenter.cs +++ b/Content.Client/ContextMenu/UI/EntityMenuUIController.cs @@ -1,10 +1,9 @@ -using System.Collections.Generic; using System.Linq; using Content.Client.CombatMode; using Content.Client.Examine; using Content.Client.Gameplay; using Content.Client.Verbs; -using Content.Client.Viewport; +using Content.Client.Verbs.UI; using Content.Shared.CCVar; using Content.Shared.CombatMode; using Content.Shared.Examine; @@ -15,14 +14,11 @@ using Robust.Client.Input; using Robust.Client.Player; using Robust.Client.State; using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controllers; using Robust.Shared.Configuration; -using Robust.Shared.GameObjects; using Robust.Shared.Input; using Robust.Shared.Input.Binding; -using Robust.Shared.IoC; -using Robust.Shared.Log; using Robust.Shared.Map; -using Robust.Shared.Maths; using Robust.Shared.Timing; namespace Content.Client.ContextMenu.UI @@ -31,11 +27,11 @@ namespace Content.Client.ContextMenu.UI /// This class handles the displaying of the entity context menu. /// /// - /// In addition to the normal functionality, this also provides functions get + /// This also provides functions to get /// a list of entities near the mouse position, add them to the context menu grouped by prototypes, and remove /// them from the menu as they move out of sight. /// - public sealed partial class EntityMenuPresenter : ContextMenuPresenter + public sealed partial class EntityMenuUIController : UIController, IOnStateEntered, IOnStateExited { [Dependency] private readonly IEntitySystemManager _systemManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!; @@ -46,11 +42,15 @@ namespace Content.Client.ContextMenu.UI [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; [Dependency] private readonly IEyeManager _eyeManager = default!; + [Dependency] private readonly ContextMenuUIController _context = default!; + [Dependency] private readonly VerbMenuUIController _verb = default!; - private readonly VerbSystem _verbSystem; - private readonly ExamineSystem _examineSystem; - private readonly TransformSystem _xform; - private readonly SharedCombatModeSystem _combatMode; + [UISystemDependency] private readonly VerbSystem _verbSystem = default!; + [UISystemDependency] private readonly ExamineSystem _examineSystem = default!; + [UISystemDependency] private readonly TransformSystem _xform = default!; + [UISystemDependency] private readonly CombatModeSystem _combatMode = default!; + + private bool _updating; /// /// This maps the currently displayed entities to the actual GUI elements. @@ -60,27 +60,26 @@ namespace Content.Client.ContextMenu.UI /// public Dictionary Elements = new(); - public EntityMenuPresenter(VerbSystem verbSystem) : base() + public void OnStateEntered(GameplayState state) { - IoCManager.InjectDependencies(this); - - _verbSystem = verbSystem; - _examineSystem = _entityManager.EntitySysManager.GetEntitySystem(); - _combatMode = _entityManager.EntitySysManager.GetEntitySystem(); - _xform = _entityManager.EntitySysManager.GetEntitySystem(); - + _updating = true; _cfg.OnValueChanged(CCVars.EntityMenuGroupingType, OnGroupingChanged, true); + _context.OnContextMouseEntered += OnMouseEntered; + _context.OnContextKeyEvent += OnKeyBindDown; CommandBinds.Builder .Bind(EngineKeyFunctions.UseSecondary, new PointerInputCmdHandler(HandleOpenEntityMenu, outsidePrediction: true)) - .Register(); + .Register(); } - public override void Dispose() + public void OnStateExited(GameplayState state) { - base.Dispose(); + _updating = false; Elements.Clear(); - CommandBinds.Unregister(); + _cfg.UnsubValueChanged(CCVars.EntityMenuGroupingType, OnGroupingChanged); + _context.OnContextMouseEntered -= OnMouseEntered; + _context.OnContextKeyEvent -= OnKeyBindDown; + CommandBinds.Unregister(); } /// @@ -89,8 +88,8 @@ namespace Content.Client.ContextMenu.UI public void OpenRootMenu(List entities) { // close any old menus first. - if (RootMenu.Visible) - Close(); + if (_context.RootMenu.Visible) + _context.Close(); var entitySpriteStates = GroupEntities(entities); var orderedStates = entitySpriteStates.ToList(); @@ -99,12 +98,30 @@ namespace Content.Client.ContextMenu.UI AddToUI(orderedStates); var box = UIBox2.FromDimensions(_userInterfaceManager.MousePositionScaled.Position, (1, 1)); - RootMenu.Open(box); + _context.RootMenu.Open(box); } - public override void OnKeyBindDown(ContextMenuElement element, GUIBoundKeyEventArgs args) + public void OnMouseEntered(ContextMenuElement element) + { + if (element is not EntityMenuElement entityElement) + return; + + // get an entity associated with this element + var entity = entityElement.Entity; + + // if there is none, this is a group, so don't open verbs + if (entity == null) + return; + + // Deleted() automatically checks for null & existence. + if (_entityManager.Deleted(entity)) + return; + + _verb.OpenVerbMenu(entity.Value, popup: element.SubMenu); + } + + public void OnKeyBindDown(ContextMenuElement element, GUIBoundKeyEventArgs args) { - base.OnKeyBindDown(element, args); if (element is not EntityMenuElement entityElement) return; @@ -116,14 +133,6 @@ namespace Content.Client.ContextMenu.UI if (_entityManager.Deleted(entity)) return; - // open verb menu? - if (args.Function == EngineKeyFunctions.UseSecondary) - { - _verbSystem.VerbMenu.OpenVerbMenu(entity.Value); - args.Handle(); - return; - } - // do examination? if (args.Function == ContentKeyFunctions.ExamineEntity) { @@ -154,9 +163,8 @@ namespace Content.Client.ContextMenu.UI inputSys.HandleInputCommand(session, func, message); } - _verbSystem.CloseAllMenus(); + _context.Close(); args.Handle(); - return; } } @@ -182,9 +190,12 @@ namespace Content.Client.ContextMenu.UI /// /// Check that entities in the context menu are still visible. If not, remove them from the context menu. /// - public void Update() + public override void FrameUpdate(FrameEventArgs args) { - if (!RootMenu.Visible) + if (!_updating || _context.RootMenu == null) + return; + + if (!_context.RootMenu.Visible) return; if (_playerManager.LocalPlayer?.ControlledEntity is not { } player || @@ -229,7 +240,8 @@ namespace Content.Client.ContextMenu.UI foreach (var entity in entityGroups[0]) { var element = new EntityMenuElement(entity); - AddElement(RootMenu, element); + element.SubMenu = new ContextMenuPopup(_context, element); + _context.AddElement(_context.RootMenu, element); Elements.TryAdd(entity, element); } return; @@ -245,7 +257,8 @@ namespace Content.Client.ContextMenu.UI // this group only has a single entity, add a simple menu element var element = new EntityMenuElement(group[0]); - AddElement(RootMenu, element); + element.SubMenu = new ContextMenuPopup(_context, element); + _context.AddElement(_context.RootMenu, element); Elements.TryAdd(group[0], element); } @@ -257,17 +270,18 @@ namespace Content.Client.ContextMenu.UI private void AddGroupToUI(List group) { EntityMenuElement element = new(); - ContextMenuPopup subMenu = new(this, element); + ContextMenuPopup subMenu = new(_context, element); foreach (var entity in group) { var subElement = new EntityMenuElement(entity); - AddElement(subMenu, subElement); + subElement.SubMenu = new ContextMenuPopup(_context, subElement); + _context.AddElement(subMenu, subElement); Elements.TryAdd(entity, subElement); } UpdateElement(element); - AddElement(RootMenu, element); + _context.AddElement(_context.RootMenu, element); } /// @@ -291,13 +305,9 @@ namespace Content.Client.ContextMenu.UI if (parent is EntityMenuElement e) UpdateElement(e); - // if the verb menu is open and targeting this entity, close it. - if (_verbSystem.VerbMenu.CurrentTarget == entity) - _verbSystem.VerbMenu.Close(); - // If this was the last entity, close the entity menu - if (RootMenu.MenuBody.ChildCount == 0) - Close(); + if (_context.RootMenu.MenuBody.ChildCount == 0) + _context.Close(); } /// @@ -376,17 +386,5 @@ namespace Content.Client.ContextMenu.UI return null; } - - public override void OpenSubMenu(ContextMenuElement element) - { - base.OpenSubMenu(element); - - // In case the verb menu is currently open, ensure that it is shown ABOVE the entity menu. - if (_verbSystem.VerbMenu.Menus.TryPeek(out var menu) && menu.Visible) - { - menu.ParentElement?.ParentMenu?.SetPositionLast(); - menu.SetPositionLast(); - } - } } } diff --git a/Content.Client/Hands/Systems/HandsSystem.cs b/Content.Client/Hands/Systems/HandsSystem.cs index 8c72b603ff..08bf64843b 100644 --- a/Content.Client/Hands/Systems/HandsSystem.cs +++ b/Content.Client/Hands/Systems/HandsSystem.cs @@ -14,6 +14,8 @@ using Robust.Shared.GameStates; using Robust.Shared.Map; using Robust.Shared.Timing; using System.Diagnostics.CodeAnalysis; +using Content.Client.Verbs.UI; +using Robust.Client.UserInterface; namespace Content.Client.Hands.Systems { @@ -22,11 +24,11 @@ namespace Content.Client.Hands.Systems { [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IUserInterfaceManager _ui = default!; [Dependency] private readonly SharedContainerSystem _containerSystem = default!; [Dependency] private readonly StrippableSystem _stripSys = default!; [Dependency] private readonly ExamineSystem _examine = default!; - [Dependency] private readonly VerbSystem _verbs = default!; public event Action? OnPlayerAddHand; public event Action? OnPlayerRemoveHand; @@ -240,9 +242,9 @@ namespace Content.Client.Hands.Systems return; } - _verbs.VerbMenu.OpenVerbMenu(entity); + _ui.GetUIController().OpenVerbMenu(entity); } - + public void UIHandAltActivateItem(string handName) { RaisePredictiveEvent(new RequestHandAltInteractEvent(handName)); diff --git a/Content.Client/Inventory/ClientInventorySystem.cs b/Content.Client/Inventory/ClientInventorySystem.cs index f71f52d809..6e500cfc69 100644 --- a/Content.Client/Inventory/ClientInventorySystem.cs +++ b/Content.Client/Inventory/ClientInventorySystem.cs @@ -3,6 +3,7 @@ using Content.Client.Examine; using Content.Client.Storage; using Content.Client.UserInterface.Controls; using Content.Client.Verbs; +using Content.Client.Verbs.UI; using Content.Shared.Clothing.Components; using Content.Shared.Hands.Components; using Content.Shared.Interaction; @@ -12,6 +13,7 @@ using Content.Shared.Inventory.Events; using JetBrains.Annotations; using Robust.Client.GameObjects; using Robust.Client.Player; +using Robust.Client.UserInterface; using Robust.Shared.Containers; using Robust.Shared.Input.Binding; using Robust.Shared.Prototypes; @@ -23,10 +25,10 @@ namespace Content.Client.Inventory { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IUserInterfaceManager _ui = default!; [Dependency] private readonly ClientClothingSystem _clothingVisualsSystem = default!; [Dependency] private readonly ExamineSystem _examine = default!; - [Dependency] private readonly VerbSystem _verbs = default!; public Action? EntitySlotUpdate = null; public Action? OnSlotAdded = null; @@ -270,7 +272,7 @@ namespace Content.Client.Inventory if (!TryGetSlotEntity(uid, slot, out var item)) return; - _verbs.VerbMenu.OpenVerbMenu(item.Value); + _ui.GetUIController().OpenVerbMenu(item.Value); } public void UIInventoryActivateItem(string slot, EntityUid uid) diff --git a/Content.Client/Storage/StorageBoundUserInterface.cs b/Content.Client/Storage/StorageBoundUserInterface.cs index c8a525ae8f..eb22307318 100644 --- a/Content.Client/Storage/StorageBoundUserInterface.cs +++ b/Content.Client/Storage/StorageBoundUserInterface.cs @@ -2,6 +2,7 @@ using Content.Client.Examine; using Content.Client.Storage.UI; using Content.Client.UserInterface.Controls; using Content.Client.Verbs; +using Content.Client.Verbs.UI; using Content.Shared.Input; using Content.Shared.Interaction; using JetBrains.Annotations; @@ -72,7 +73,7 @@ namespace Content.Client.Storage } else if (args.Function == EngineKeyFunctions.UseSecondary) { - entitySys.GetEntitySystem().VerbMenu.OpenVerbMenu(entity); + IoCManager.Resolve().GetUIController().OpenVerbMenu(entity); } else if (args.Function == ContentKeyFunctions.ActivateItemInWorld) { diff --git a/Content.Client/UserInterface/Systems/Admin/AdminUIController.cs b/Content.Client/UserInterface/Systems/Admin/AdminUIController.cs index b9be5533a4..c53f7d6752 100644 --- a/Content.Client/UserInterface/Systems/Admin/AdminUIController.cs +++ b/Content.Client/UserInterface/Systems/Admin/AdminUIController.cs @@ -5,6 +5,7 @@ using Content.Client.Administration.UI.Tabs.PlayerTab; using Content.Client.Gameplay; using Content.Client.UserInterface.Controls; using Content.Client.Verbs; +using Content.Client.Verbs.UI; using Content.Shared.Input; using JetBrains.Annotations; using Robust.Client.Console; @@ -26,8 +27,7 @@ public sealed class AdminUIController : UIController, IOnStateEntered UIManager.GetActiveUIWidgetOrNull()?.AdminButton; @@ -135,7 +135,7 @@ public sealed class AdminUIController : UIController, IOnStateEntered /// - /// In addition to the normal functionality, this also provides functions + /// 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 + public sealed class VerbMenuUIController : UIController, IOnStateEntered, IOnStateExited { [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; + [Dependency] private readonly ContextMenuUIController _context = default!; - private readonly CombatModeSystem _combatMode; - private readonly VerbSystem _verbSystem; + [UISystemDependency] private readonly CombatModeSystem _combatMode = default!; + [UISystemDependency] private readonly VerbSystem _verbSystem = default!; public EntityUid CurrentTarget; public SortedSet CurrentVerbs = new(); - public VerbMenuPresenter(CombatModeSystem combatMode, VerbSystem verbSystem) + /// + /// Separate from , since we can open a verb menu as a submenu + /// of an entity menu element. If that happens, we need to be aware and close it properly. + /// + public ContextMenuPopup? OpenMenu = null; + + public void OnStateEntered(GameplayState state) { - IoCManager.InjectDependencies(this); - _combatMode = combatMode; - _verbSystem = verbSystem; + _context.OnContextKeyEvent += OnKeyBindDown; + _context.OnContextClosed += Close; + _verbSystem.OnVerbsResponse += HandleVerbsResponse; + } + + public void OnStateExited(GameplayState state) + { + _context.OnContextKeyEvent -= OnKeyBindDown; + _context.OnContextClosed -= Close; + if (_verbSystem != null) + _verbSystem.OnVerbsResponse -= HandleVerbsResponse; + Close(); } /// - /// Open a verb menu and fill it work verbs applicable to the given target entity. + /// Open a verb menu and fill it with verbs applicable to the given target entity. /// /// Entity to get verbs on. /// Used to force showing all verbs (mostly for admins). - public void OpenVerbMenu(EntityUid target, bool force = false) + /// + /// If this is not null, verbs will be placed into the given popup instead. + /// + public void OpenVerbMenu(EntityUid target, bool force = false, ContextMenuPopup? popup=null) { if (_playerManager.LocalPlayer?.ControlledEntity is not {Valid: true} user || _combatMode.IsInCombatMode(user)) @@ -55,53 +76,59 @@ namespace Content.Client.Verbs.UI Close(); + var menu = popup ?? _context.RootMenu; + menu.MenuBody.DisposeAllChildren(); + CurrentTarget = target; CurrentVerbs = _verbSystem.GetVerbs(target, user, Verb.VerbTypes, force); + OpenMenu = menu; // Fill in client-side verbs. - FillVerbPopup(); + FillVerbPopup(menu); // Add indicator that some verbs may be missing. // I long for the day when verbs will all be predicted and this becomes unnecessary. if (!target.IsClientSide()) { - AddElement(RootMenu, new ContextMenuElement(Loc.GetString("verb-system-waiting-on-server-text"))); + _context.AddElement(menu, new ContextMenuElement(Loc.GetString("verb-system-waiting-on-server-text"))); } - // Show the menu - RootMenu.SetPositionLast(); + // if popup isn't null (ie we are opening out of an entity menu element), + // assume that that is going to handle opening the submenu properly + if (popup != null) + return; + + // Show the menu at mouse pos + menu.SetPositionLast(); var box = UIBox2.FromDimensions(_userInterfaceManager.MousePositionScaled.Position, (1, 1)); - RootMenu.Open(box); + menu.Open(box); } /// /// Fill the verb pop-up using the verbs stored in /// - private void FillVerbPopup() + private void FillVerbPopup(ContextMenuPopup popup) { - if (RootMenu == null) - return; - HashSet listedCategories = new(); foreach (var verb in CurrentVerbs) { if (verb.Category == null) { var element = new VerbMenuElement(verb); - AddElement(RootMenu, element); + _context.AddElement(popup, element); } else if (listedCategories.Add(verb.Category.Text)) - AddVerbCategory(verb.Category); + AddVerbCategory(verb.Category, popup); } - RootMenu.InvalidateMeasure(); + popup.InvalidateMeasure(); } /// /// Add a verb category button to the pop-up /// - public void AddVerbCategory(VerbCategory category) + public void AddVerbCategory(VerbCategory category, ContextMenuPopup popup) { // Get a list of the verbs in this category List verbsInCategory = new(); @@ -119,10 +146,10 @@ namespace Content.Client.Verbs.UI return; var element = new VerbMenuElement(category, verbsInCategory[0].TextStyleClass); - AddElement(RootMenu, element); + _context.AddElement(popup, element); // Create the pop-up that appears when hovering over this element - element.SubMenu = new ContextMenuPopup(this, element); + element.SubMenu = new ContextMenuPopup(_context, element); foreach (var verb in verbsInCategory) { var subElement = new VerbMenuElement(verb) @@ -130,7 +157,7 @@ namespace Content.Client.Verbs.UI IconVisible = drawIcons, TextVisible = !category.IconsOnly }; - AddElement(element.SubMenu, subElement); + _context.AddElement(element.SubMenu, subElement); } element.SubMenu.MenuBody.Columns = category.Columns; @@ -139,23 +166,23 @@ namespace Content.Client.Verbs.UI /// /// Add verbs from the server to and update the verb menu. /// - public void AddServerVerbs(List? verbs) + public void AddServerVerbs(List? verbs, ContextMenuPopup popup) { - RootMenu.MenuBody.DisposeAllChildren(); + popup.MenuBody.DisposeAllChildren(); // Verbs may be null if the server does not think we can see the target entity. This **should** not happen. if (verbs == null) { // remove "waiting for server..." and inform user that something went wrong. - AddElement(RootMenu, new ContextMenuElement(Loc.GetString("verb-system-null-server-response"))); + _context.AddElement(popup, new ContextMenuElement(Loc.GetString("verb-system-null-server-response"))); return; } CurrentVerbs.UnionWith(verbs); - FillVerbPopup(); + FillVerbPopup(popup); } - public override void OnKeyBindDown(ContextMenuElement element, GUIBoundKeyEventArgs args) + public void OnKeyBindDown(ContextMenuElement element, GUIBoundKeyEventArgs args) { if (args.Function != EngineKeyFunctions.Use && args.Function != ContentKeyFunctions.ActivateItemInWorld) return; @@ -185,7 +212,7 @@ namespace Content.Client.Verbs.UI if (verbElement.SubMenu.MenuBody.ChildCount != 1 || verbElement.SubMenu.MenuBody.Children.First() is not VerbMenuElement verbMenuElement) { - OpenSubMenu(verbElement); + _context.OpenSubMenu(verbElement); return; } @@ -200,11 +227,11 @@ namespace Content.Client.Verbs.UI if (verbElement.SubMenu == null) { var popupElement = new ConfirmationMenuElement(verb, "Confirm"); - verbElement.SubMenu = new ContextMenuPopup(this, verbElement); - AddElement(verbElement.SubMenu, popupElement); + verbElement.SubMenu = new ContextMenuPopup(_context, verbElement); + _context.AddElement(verbElement.SubMenu, popupElement); } - OpenSubMenu(verbElement); + _context.OpenSubMenu(verbElement); } else { @@ -212,11 +239,28 @@ namespace Content.Client.Verbs.UI } } + private void Close() + { + if (OpenMenu == null) + return; + + OpenMenu.Close(); + OpenMenu = null; + } + + private void HandleVerbsResponse(VerbsResponseEvent msg) + { + if (OpenMenu == null || !OpenMenu.Visible || CurrentTarget != msg.Entity) + return; + + AddServerVerbs(msg.Verbs, OpenMenu); + } + private void ExecuteVerb(Verb verb) { _verbSystem.ExecuteVerb(CurrentTarget, verb); if (verb.CloseMenu) - _verbSystem.CloseAllMenus(); + _context.Close(); } } } diff --git a/Content.Client/Verbs/VerbSystem.cs b/Content.Client/Verbs/VerbSystem.cs index 8a0b69d774..c73a319571 100644 --- a/Content.Client/Verbs/VerbSystem.cs +++ b/Content.Client/Verbs/VerbSystem.cs @@ -17,6 +17,7 @@ using Robust.Shared.Map; using Robust.Shared.Utility; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Robust.Client.UserInterface; namespace Content.Client.Verbs { @@ -36,9 +37,6 @@ namespace Content.Client.Verbs /// public const float EntityMenuLookupSize = 0.25f; - public EntityMenuPresenter EntityMenu = default!; - public VerbMenuPresenter VerbMenu = default!; - [Dependency] private readonly IEyeManager _eyeManager = default!; /// @@ -46,41 +44,13 @@ namespace Content.Client.Verbs /// public MenuVisibility Visibility; + public Action? OnVerbsResponse; + public override void Initialize() { base.Initialize(); - UpdatesOutsidePrediction = true; - - SubscribeNetworkEvent(Reset); SubscribeNetworkEvent(HandleVerbResponse); - - EntityMenu = new(this); - VerbMenu = new(_combatMode, this); - } - - public void Reset(RoundRestartCleanupEvent ev) - { - CloseAllMenus(); - } - - public override void Shutdown() - { - base.Shutdown(); - EntityMenu?.Dispose(); - VerbMenu?.Dispose(); - } - - public override void FrameUpdate(float frameTime) - { - base.FrameUpdate(frameTime); - EntityMenu?.Update(); - } - - public void CloseAllMenus() - { - EntityMenu.Close(); - VerbMenu.Close(); } /// @@ -259,10 +229,7 @@ namespace Content.Client.Verbs private void HandleVerbResponse(VerbsResponseEvent msg) { - if (!VerbMenu.RootMenu.Visible || VerbMenu.CurrentTarget != msg.Entity) - return; - - VerbMenu.AddServerVerbs(msg.Verbs); + OnVerbsResponse?.Invoke(msg); } }