ContextMenu (#3286)

* ContextMenu

* Updating to WPF.

* Updating to WPF.

* Margins
This commit is contained in:
Daniel Castro Razo
2021-02-25 19:42:16 -06:00
committed by GitHub
parent 51182c8469
commit f30a4d8a52
10 changed files with 1059 additions and 231 deletions

View File

@@ -0,0 +1,336 @@
#nullable enable
using System;
using System.Linq;
using System.Threading;
using Content.Client.State;
using Content.Client.UserInterface;
using Content.Client.UserInterface.ContextMenu;
using Content.Client.Utility;
using Content.Shared;
using Content.Shared.GameObjects.Verbs;
using Content.Shared.Input;
using Robust.Client.GameObjects;
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 Timer = Robust.Shared.Timing.Timer;
namespace Content.Client.GameObjects.EntitySystems
{
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!;
private readonly IContextMenuView _contextMenuView;
private readonly VerbSystem _verbSystem;
private bool _playerCanSeeThroughContainers;
private MapCoordinates _mapCoordinates;
private CancellationTokenSource? _cancellationTokenSource;
public ContextMenuPresenter(VerbSystem verbSystem)
{
IoCManager.InjectDependencies(this);
_verbSystem = verbSystem;
_verbSystem.ToggleContextMenu += SystemOnToggleContextMenu;
_verbSystem.ToggleContainerVisibility += SystemOnToggleContainerVisibility;
_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);
}
#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;
_cancellationTokenSource?.Cancel();
_cancellationTokenSource = new();
Timer.Spawn(e.HoverDelay, () =>
{
_verbSystem.CloseGroupMenu();
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);
}
}, _cancellationTokenSource.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.TryPullObject || args.Function == ContentKeyFunctions.MovePulledObject)
{
var inputSys = _systemManager.GetEntitySystem<InputSystem>();
var func = args.Function;
var funcId = _inputManager.NetworkBindMap.KeyFunctionID(func);
var message = new FullInputCmdMessage(_gameTiming.CurTick, _gameTiming.TickFraction, funcId,
BoundKeyState.Down, firstEntity.Transform.Coordinates, args.PointerLocation, firstEntity.Uid);
var session = _playerManager.LocalPlayer?.Session;
if (session != null)
{
inputSys.HandleInputCommand(session, func, message);
}
CloseAllMenus();
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);
e.OutlineComponent?.UpdateInRange(inRange);
}
}
private void OnMouseEnteredSingle(object? sender, SingleContextElement e)
{
_cancellationTokenSource?.Cancel();
var entity = e.ContextEntity;
_verbSystem.CloseGroupMenu();
OnCloseChildMenu(sender, e.ParentMenu?.Depth ?? 0);
if (entity.Deleted) return;
var localPlayer = _playerManager.LocalPlayer;
if (localPlayer?.ControlledEntity == null) return;
e.OutlineComponent?.OnMouseEnter(localPlayer.InRangeUnobstructed(entity, ignoreInsideBlocker: true));
if (e.SpriteComp != null)
{
e.SpriteComp.DrawDepth = (int) Shared.GameObjects.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);
return;
}
if (args.Function == ContentKeyFunctions.ExamineEntity)
{
_systemManager.GetEntitySystem<ExamineSystem>().DoExamine(entity);
return;
}
if (args.Function == EngineKeyFunctions.Use || args.Function == ContentKeyFunctions.Point ||
args.Function == ContentKeyFunctions.TryPullObject || args.Function == ContentKeyFunctions.MovePulledObject)
{
var inputSys = _systemManager.GetEntitySystem<InputSystem>();
var func = args.Function;
var funcId = _inputManager.NetworkBindMap.KeyFunctionID(func);
var message = new FullInputCmdMessage(_gameTiming.CurTick, _gameTiming.TickFraction, funcId,
BoundKeyState.Down, entity.Transform.Coordinates, args.PointerLocation, entity.Uid);
var session = _playerManager.LocalPlayer?.Session;
if (session != null)
{
inputSys.HandleInputCommand(session, func, message);
}
CloseAllMenus();
return;
}
if (_itemSlotManager.OnButtonPressed(args, single.ContextEntity))
{
CloseAllMenus();
}
}
#endregion
#region Model Updates
private void SystemOnToggleContainerVisibility(object? sender, bool args)
{
_playerCanSeeThroughContainers = args;
}
private void SystemOnToggleContextMenu(object? sender, PointerInputCmdHandler.PointerInputCmdArgs args)
{
if (_stateManager.CurrentState is not GameScreenBase)
{
return;
}
var playerEntity = _playerManager.LocalPlayer?.ControlledEntity;
if (playerEntity == null)
{
return;
}
_mapCoordinates = args.Coordinates.ToMap(_entityManager);
if (!_verbSystem.TryGetContextEntities(playerEntity, _mapCoordinates, out var entities))
{
return;
}
entities = entities.Where(CanSeeOnContextMenu).ToList();
if (entities.Count > 0)
{
_contextMenuView.AddRootMenu(entities);
}
}
public void HandleMoveEvent(MoveEvent ev)
{
if (_contextMenuView.Elements.Count == 0) return;
var entity = ev.Sender;
if (_contextMenuView.Elements.ContainsKey(entity))
{
if (!entity.Transform.MapPosition.InRange(_mapCoordinates, 1.0f))
{
_contextMenuView.RemoveEntity(entity);
}
}
}
public void Update()
{
if (_contextMenuView.Elements.Count == 0) return;
foreach (var entity in _contextMenuView.Elements.Keys.ToList())
{
if (entity.Deleted || !_playerCanSeeThroughContainers && entity.IsInContainer())
{
_contextMenuView.RemoveEntity(entity);
}
}
}
#endregion
private bool CanSeeOnContextMenu(IEntity entity)
{
if (!entity.TryGetComponent(out ISpriteComponent? spriteComponent) || !spriteComponent.Visible)
{
return false;
}
if (entity.GetAllComponents<IShowContextMenu>().Any(s => !s.ShowContextMenu(entity)))
{
return false;
}
return _playerCanSeeThroughContainers || !entity.TryGetContainer(out var container) || container.ShowContents;
}
private void CloseAllMenus()
{
_contextMenuView.CloseContextPopups();
_verbSystem.CloseGroupMenu();
_verbSystem.CloseVerbMenu();
}
public void Dispose()
{
_verbSystem.ToggleContextMenu -= SystemOnToggleContextMenu;
_verbSystem.ToggleContainerVisibility -= SystemOnToggleContainerVisibility;
_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;
}
}
}

View File

@@ -1,34 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using Content.Client.State;
using Content.Client.UserInterface;
using Content.Client.Utility;
using Content.Shared.GameObjects.EntitySystemMessages;
using Content.Shared.GameObjects.Verbs;
using Content.Shared.GameTicking;
using Content.Shared.Input;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Timer = Robust.Shared.Timing.Timer;
@@ -37,52 +30,67 @@ namespace Content.Client.GameObjects.EntitySystems
[UsedImplicitly]
public sealed class VerbSystem : SharedVerbSystem, IResettingEntitySystem
{
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IItemSlotManager _itemSlotManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
private EntityList _currentEntityList;
private ContextMenuPresenter _contextMenuPresenter;
public event EventHandler<PointerInputCmdHandler.PointerInputCmdArgs> ToggleContextMenu;
public event EventHandler<bool> ToggleContainerVisibility;
private VerbPopup _currentVerbListRoot;
private VerbPopup _currentGroupList;
private EntityUid _currentEntity;
private bool IsAnyContextMenuOpen => _currentEntityList != null || _currentVerbListRoot != null;
private bool _playerCanSeeThroughContainers;
// TODO: Move presenter out of the system
// TODO: Separate the rest of the UI from the logic
public override void Initialize()
{
base.Initialize();
IoCManager.InjectDependencies(this);
SubscribeNetworkEvent<VerbSystemMessages.VerbsResponseMessage>(FillEntityPopup);
SubscribeNetworkEvent<PlayerContainerVisibilityMessage>(HandleContainerVisibilityMessage);
IoCManager.InjectDependencies(this);
_contextMenuPresenter = new ContextMenuPresenter(this);
SubscribeLocalEvent<MoveEvent>(_contextMenuPresenter.HandleMoveEvent);
CommandBinds.Builder
.Bind(ContentKeyFunctions.OpenContextMenu,
new PointerInputCmdHandler(OnOpenContextMenu))
new PointerInputCmdHandler(HandleOpenContextMenu))
.Register<VerbSystem>();
}
public override void Shutdown()
{
UnsubscribeLocalEvent<MoveEvent>();
_contextMenuPresenter?.Dispose();
CommandBinds.Unregister<VerbSystem>();
base.Shutdown();
}
public void Reset()
{
_playerCanSeeThroughContainers = false;
ToggleContainerVisibility?.Invoke(this, false);
}
private bool HandleOpenContextMenu(in PointerInputCmdHandler.PointerInputCmdArgs args)
{
if (args.State == BoundKeyState.Down)
{
ToggleContextMenu?.Invoke(this, args);
}
return true;
}
private void HandleContainerVisibilityMessage(PlayerContainerVisibilityMessage ev)
{
_playerCanSeeThroughContainers = ev.CanSeeThrough;
ToggleContainerVisibility?.Invoke(this, ev.CanSeeThrough);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
_contextMenuPresenter?.Update();
}
public void OpenContextMenu(IEntity entity, ScreenCoordinates screenCoordinates)
@@ -99,7 +107,7 @@ namespace Content.Client.GameObjects.EntitySystems
if (!entity.Uid.IsClientSide())
{
_currentVerbListRoot.List.AddChild(new Label {Text = "Waiting on Server..."});
_currentVerbListRoot.List.AddChild(new Label { Text = Loc.GetString("Waiting on Server...") });
RaiseNetworkEvent(new VerbSystemMessages.RequestVerbsMessage(_currentEntity));
}
@@ -107,84 +115,7 @@ namespace Content.Client.GameObjects.EntitySystems
_currentVerbListRoot.Open(box);
}
public bool CanSeeOnContextMenu(IEntity entity)
{
if (!entity.TryGetComponent(out SpriteComponent sprite) || !sprite.Visible)
{
return false;
}
if (entity.GetAllComponents<IShowContextMenu>().Any(s => !s.ShowContextMenu(entity)))
{
return false;
}
if (!_playerCanSeeThroughContainers &&
entity.TryGetContainer(out var container) &&
!container.ShowContents)
{
return false;
}
return true;
}
private bool OnOpenContextMenu(in PointerInputCmdHandler.PointerInputCmdArgs args)
{
if (IsAnyContextMenuOpen)
{
CloseAllMenus();
return true;
}
if (_stateManager.CurrentState is not GameScreenBase)
{
return false;
}
var mapCoordinates = args.Coordinates.ToMap(EntityManager);
var playerEntity = _playerManager.LocalPlayer?.ControlledEntity;
if (playerEntity == null || !TryGetContextEntities(playerEntity, mapCoordinates, out var entities))
{
return false;
}
_currentEntityList = new EntityList();
_currentEntityList.OnPopupHide += CloseAllMenus;
var first = true;
foreach (var entity in entities)
{
if (!CanSeeOnContextMenu(entity))
{
continue;
}
if (!first)
{
_currentEntityList.List.AddChild(new PanelContainer
{
MinSize = (0, 2),
PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#333")}
});
}
var debugEnabled = _userInterfaceManager.DebugMonitors.Visible;
_currentEntityList.List.AddChild(new EntityButton(this, entity, debugEnabled));
first = false;
}
_userInterfaceManager.ModalRoot.AddChild(_currentEntityList);
_currentEntityList.List.Measure(Vector2.Infinity);
var size = _currentEntityList.List.DesiredSize;
var box = UIBox2.FromDimensions(_userInterfaceManager.MousePositionScaled, size);
_currentEntityList.Open(box);
return true;
}
private void OnContextButtonPressed(IEntity entity)
public void OnContextButtonPressed(IEntity entity)
{
OpenContextMenu(entity, new ScreenCoordinates(_userInterfaceManager.MousePositionScaled));
}
@@ -286,7 +217,7 @@ namespace Content.Client.GameObjects.EntitySystems
vBox.AddChild(new PanelContainer
{
MinSize = (0, 2),
PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#333")}
PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#333") }
});
}
@@ -308,7 +239,7 @@ namespace Content.Client.GameObjects.EntitySystems
vBox.AddChild(new PanelContainer
{
MinSize = (0, 2),
PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#333")}
PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#333") }
});
}
@@ -321,7 +252,7 @@ namespace Content.Client.GameObjects.EntitySystems
else
{
var panel = new PanelContainer();
panel.AddChild(new Label {Text = "No verbs!"});
panel.AddChild(new Label { Text = Loc.GetString("No verbs!") });
vBox.AddChild(panel);
}
}
@@ -330,7 +261,7 @@ namespace Content.Client.GameObjects.EntitySystems
{
var button = new VerbButton
{
Text = data.Text,
Text = Loc.GetString(data.Text),
Disabled = data.Disabled
};
@@ -364,31 +295,25 @@ namespace Content.Client.GameObjects.EntitySystems
return new VerbGroupButton(this, verbButtons, icon)
{
Text = text,
Text = Loc.GetString(text),
};
}
private void CloseVerbMenu()
public void CloseVerbMenu()
{
_currentVerbListRoot?.Dispose();
_currentVerbListRoot = null;
_currentEntity = EntityUid.Invalid;
}
private void CloseEntityList()
{
_currentEntityList?.Dispose();
_currentEntityList = null;
}
private void CloseAllMenus()
{
CloseVerbMenu();
CloseEntityList();
// CloseContextPopups();
CloseGroupMenu();
}
private void CloseGroupMenu()
public void CloseGroupMenu()
{
_currentGroupList?.Dispose();
_currentGroupList = null;
@@ -399,20 +324,6 @@ namespace Content.Client.GameObjects.EntitySystems
return _playerManager.LocalPlayer.ControlledEntity;
}
private sealed class EntityList : Popup
{
public VBoxContainer List { get; }
public EntityList()
{
AddChild(new PanelContainer
{
Children = {(List = new VBoxContainer())},
PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#111E")}
});
}
}
private sealed class VerbPopup : Popup
{
public VBoxContainer List { get; }
@@ -427,102 +338,6 @@ namespace Content.Client.GameObjects.EntitySystems
}
}
private sealed class EntityButton : Control
{
private readonly VerbSystem _master;
private readonly IEntity _entity;
public EntityButton(VerbSystem master, IEntity entity, bool showUid)
{
_master = master;
_entity = entity;
MouseFilter = MouseFilterMode.Stop;
var control = new HBoxContainer {SeparationOverride = 6};
if (entity.TryGetComponent(out ISpriteComponent sprite))
{
control.AddChild(new SpriteView {Sprite = sprite});
}
var text = entity.Name;
if (showUid)
{
text = $"{text} ({entity.Uid})";
}
control.AddChild(new Label
{
Margin = new Thickness(4, 0),
Text = text
});
AddChild(control);
}
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
{
base.KeyBindDown(args);
if (args.Function == ContentKeyFunctions.OpenContextMenu)
{
_master.OnContextButtonPressed(_entity);
return;
}
if (args.Function == EngineKeyFunctions.Use ||
args.Function == ContentKeyFunctions.Point ||
args.Function == ContentKeyFunctions.TryPullObject ||
args.Function == ContentKeyFunctions.MovePulledObject)
{
// TODO: Remove an entity from the menu when it is deleted
if (_entity.Deleted)
{
_master.CloseAllMenus();
return;
}
var inputSys = _master.EntitySystemManager.GetEntitySystem<InputSystem>();
var func = args.Function;
var funcId = _master._inputManager.NetworkBindMap.KeyFunctionID(args.Function);
var message = new FullInputCmdMessage(_master._gameTiming.CurTick, _master._gameTiming.TickFraction,
funcId, BoundKeyState.Down,
_entity.Transform.Coordinates,
args.PointerLocation, _entity.Uid);
// client side command handlers will always be sent the local player session.
var session = _master._playerManager.LocalPlayer.Session;
inputSys.HandleInputCommand(session, func, message);
_master.CloseAllMenus();
return;
}
if (args.Function == ContentKeyFunctions.ExamineEntity)
{
Get<ExamineSystem>().DoExamine(_entity);
return;
}
if (_master._itemSlotManager.OnButtonPressed(args, _entity))
{
_master.CloseAllMenus();
}
}
protected override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
if (UserInterfaceManager.CurrentlyHovered == this)
{
handle.DrawRect(PixelSizeBox, Color.DarkSlateGray);
}
}
}
private sealed class VerbButton : BaseButton
{
private readonly Label _label;
@@ -611,7 +426,7 @@ namespace Content.Client.GameObjects.EntitySystems
(_label = new Label
{
HorizontalExpand = true
SizeFlagsHorizontal = SizeFlags.FillExpand
}),
// Padding