Refactor Context Menus and make them use XAML & stylesheets (#4768)
* XAML verb menu * fix ghost FOV * spacing * rename missed "ContextMenu"->"EntityMenu" instances * move visibility checks to verb system * update comment * Remove CanSeeContainerCheck * use ScrollContainer measure option * MaxWidth / texxt line wrapping * verb category default Now when you click on a verb category, it should default to running the first member of that category. This makes it much more convenient to eject/insert when there is only a single option * only apply style to first verb category entry * Use new visibility flags * FoV -> Fov * Revert "only apply style to first verb category entry" This reverts commit 9a6a17dba600e3ae0421caed59fcab145c260c99. * make all entity menu visibility checks clientside * Fix empty unbuckle category * fix merge
This commit is contained in:
@@ -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<IConfigurationManager>();
|
||||
var cvar = CCVars.ContextMenuGroupingType;
|
||||
var cvar = CCVars.EntityMenuGroupingType;
|
||||
|
||||
configurationManager.SetCVar(cvar, id);
|
||||
shell.WriteLine($"Context Menu Grouping set to type: {configurationManager.GetCVar(cvar)}");
|
||||
54
Content.Client/Commands/SetMenuVisibilityCommand.cs
Normal file
54
Content.Client/Commands/SetMenuVisibilityCommand.cs
Normal file
@@ -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<VerbSystem>().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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Client.Interactable.Components;
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Stylesheets;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
using Vector2 = Robust.Shared.Maths.Vector2;
|
||||
|
||||
namespace Content.Client.ContextMenu.UI
|
||||
{
|
||||
public abstract class ContextMenuElement : Control
|
||||
{
|
||||
private static readonly Color HoverColor = Color.DarkSlateGray;
|
||||
protected internal readonly ContextMenuPopup? ParentMenu;
|
||||
|
||||
protected ContextMenuElement(ContextMenuPopup? parentMenu)
|
||||
{
|
||||
ParentMenu = parentMenu;
|
||||
MouseFilter = MouseFilterMode.Stop;
|
||||
}
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
if (UserInterfaceManager.CurrentlyHovered == this)
|
||||
{
|
||||
handle.DrawRect(PixelSizeBox, HoverColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SingleContextElement : ContextMenuElement
|
||||
{
|
||||
public event Action? OnMouseHovering;
|
||||
public event Action? OnExitedTree;
|
||||
|
||||
public IEntity ContextEntity{ get; }
|
||||
public readonly StackContextElement? Pre;
|
||||
|
||||
public ISpriteComponent? SpriteComp { get; }
|
||||
public InteractionOutlineComponent? OutlineComponent { get; }
|
||||
public int OriginalDrawDepth { get; }
|
||||
public bool DrawOutline { get; set; }
|
||||
|
||||
public SingleContextElement(IEntity entity, StackContextElement? pre, ContextMenuPopup? parentMenu) : base(parentMenu)
|
||||
{
|
||||
Pre = pre;
|
||||
ContextEntity = entity;
|
||||
if (ContextEntity.TryGetComponent(out ISpriteComponent? sprite))
|
||||
{
|
||||
SpriteComp = sprite;
|
||||
OriginalDrawDepth = SpriteComp.DrawDepth;
|
||||
}
|
||||
OutlineComponent = ContextEntity.GetComponentOrNull<InteractionOutlineComponent>();
|
||||
|
||||
AddChild(
|
||||
new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
new LayoutContainer
|
||||
{
|
||||
Children = { new SpriteView { Sprite = SpriteComp } }
|
||||
},
|
||||
new Label
|
||||
{
|
||||
Text = Loc.GetString(UserInterfaceManager.DebugMonitors.Visible ? $"{ContextEntity.Name} ({ContextEntity.Uid})" : ContextEntity.Name)
|
||||
}
|
||||
}, Margin = new Thickness(0,0,10,0)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
if (UserInterfaceManager.CurrentlyHovered == this)
|
||||
{
|
||||
OnMouseHovering?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
OnExitedTree?.Invoke();
|
||||
base.ExitedTree();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class StackContextElement : ContextMenuElement
|
||||
{
|
||||
public event Action? OnExitedTree;
|
||||
|
||||
public HashSet<IEntity> ContextEntities { get; }
|
||||
public readonly StackContextElement? Pre;
|
||||
|
||||
private readonly SpriteView _spriteView;
|
||||
private readonly Label _label;
|
||||
|
||||
public int EntitiesCount => ContextEntities.Count;
|
||||
|
||||
public StackContextElement(IEnumerable<IEntity> entities, StackContextElement? pre, ContextMenuPopup? parentMenu)
|
||||
: base(parentMenu)
|
||||
{
|
||||
Pre = pre;
|
||||
ContextEntities = new(entities);
|
||||
_spriteView = new SpriteView
|
||||
{
|
||||
Sprite = ContextEntities.First().GetComponent<ISpriteComponent>()
|
||||
};
|
||||
_label = new Label
|
||||
{
|
||||
Text = Loc.GetString(ContextEntities.Count.ToString()),
|
||||
StyleClasses = { StyleNano.StyleClassContextMenuCount }
|
||||
};
|
||||
|
||||
LayoutContainer.SetAnchorPreset(_label, LayoutContainer.LayoutPreset.BottomRight);
|
||||
LayoutContainer.SetGrowHorizontal(_label, LayoutContainer.GrowDirection.Begin);
|
||||
LayoutContainer.SetGrowVertical(_label, LayoutContainer.GrowDirection.Begin);
|
||||
|
||||
AddChild(
|
||||
new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
SeparationOverride = 6,
|
||||
Children =
|
||||
{
|
||||
new LayoutContainer { Children = { _spriteView, _label } },
|
||||
new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
SeparationOverride = 6,
|
||||
Children =
|
||||
{
|
||||
new Label
|
||||
{
|
||||
Text = Loc.GetString(ContextEntities.First().Name)
|
||||
},
|
||||
new TextureRect
|
||||
{
|
||||
Texture = IoCManager.Resolve<IResourceCache>().GetTexture("/Textures/Interface/VerbIcons/group.svg.192dpi.png"),
|
||||
TextureScale = (0.5f, 0.5f),
|
||||
Stretch = TextureRect.StretchMode.KeepCentered,
|
||||
}
|
||||
}
|
||||
}
|
||||
}, Margin = new Thickness(0,0,10,0)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
OnExitedTree?.Invoke();
|
||||
base.ExitedTree();
|
||||
}
|
||||
|
||||
public void RemoveEntity(IEntity entity)
|
||||
{
|
||||
ContextEntities.Remove(entity);
|
||||
|
||||
_label.Text = Loc.GetString(ContextEntities.Count.ToString());
|
||||
_spriteView.Sprite = ContextEntities.FirstOrDefault(e => !e.Deleted)?.GetComponent<ISpriteComponent>();
|
||||
}
|
||||
}
|
||||
|
||||
public class ContextMenuPopup : Popup
|
||||
{
|
||||
public static readonly Color ButtonColor = Color.FromHex("#1119");
|
||||
public static readonly Color BackgroundColor = Color.FromHex("#333E");
|
||||
|
||||
public const int MaxItemsBeforeScroll = 10;
|
||||
public const int MarginSize = 2;
|
||||
public const int ButtonHeight = 32;
|
||||
|
||||
public BoxContainer List { get; }
|
||||
public ScrollContainer Scroll { get; }
|
||||
public int Depth { get; }
|
||||
|
||||
public ContextMenuPopup(int depth = 0)
|
||||
{
|
||||
MaxHeight = MaxItemsBeforeScroll * (ButtonHeight + 2*MarginSize);
|
||||
|
||||
Depth = depth;
|
||||
List = new() { Orientation = LayoutOrientation.Vertical };
|
||||
Scroll = new()
|
||||
{
|
||||
HScrollEnabled = false,
|
||||
Children = { List }
|
||||
};
|
||||
AddChild(new PanelContainer
|
||||
{
|
||||
Children = { Scroll },
|
||||
PanelOverride = new StyleBoxFlat { BackgroundColor = BackgroundColor }
|
||||
});
|
||||
}
|
||||
|
||||
public void AddToMenu(Control element)
|
||||
{
|
||||
List.AddChild(new PanelContainer
|
||||
{
|
||||
Children = { element },
|
||||
Margin = new Thickness(MarginSize, MarginSize, MarginSize, MarginSize),
|
||||
PanelOverride = new StyleBoxFlat { BackgroundColor = ButtonColor }
|
||||
});
|
||||
}
|
||||
|
||||
public void RemoveFromMenu(ContextMenuElement element)
|
||||
{
|
||||
if (element.Parent != null)
|
||||
{
|
||||
List.RemoveChild(element.Parent);
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
protected override Vector2 MeasureOverride(Vector2 availableSize)
|
||||
{
|
||||
if (List.ChildCount == 0)
|
||||
{
|
||||
return Vector2.Zero;
|
||||
}
|
||||
|
||||
Scroll.Measure(availableSize);
|
||||
var size = List.DesiredSize;
|
||||
|
||||
// account for scroll bar width
|
||||
if (size.Y > MaxHeight)
|
||||
{
|
||||
// Scroll._vScrollBar is private and ScrollContainer gives no size information :/
|
||||
// 10 = Scroll._vScrollBar.DesiredSize
|
||||
size.X += 10;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Content.Client/ContextMenu/UI/ContextMenuElement.xaml
Normal file
26
Content.Client/ContextMenu/UI/ContextMenuElement.xaml
Normal file
@@ -0,0 +1,26 @@
|
||||
<ContainerButton
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="clr-namespace:Content.Client.ContextMenu.UI"
|
||||
MinHeight="{x:Static ui:ContextMenuElement.ElementHeight}">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Control
|
||||
Name="Icon"
|
||||
SetWidth="{x:Static ui:ContextMenuElement.ElementHeight}"
|
||||
SetHeight="{x:Static ui:ContextMenuElement.ElementHeight}"/>
|
||||
<RichTextLabel
|
||||
Name="Label"
|
||||
MaxWidth="300"
|
||||
HorizontalExpand="True"
|
||||
VerticalAlignment="Center"
|
||||
Margin ="4 0 4 0"/>
|
||||
<TextureRect
|
||||
Name="ExpansionIndicator"
|
||||
HorizontalAlignment="Right"
|
||||
Stretch="KeepCentered"
|
||||
TextureScale="0.5 0.5"
|
||||
SetWidth="{x:Static ui:ContextMenuElement.ElementHeight}"
|
||||
SetHeight="{x:Static ui:ContextMenuElement.ElementHeight}"
|
||||
Visible ="false"/>
|
||||
</BoxContainer>
|
||||
</ContainerButton>
|
||||
101
Content.Client/ContextMenu/UI/ContextMenuElement.xaml.cs
Normal file
101
Content.Client/ContextMenu/UI/ContextMenuElement.xaml.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using Content.Client.Resources;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.ContextMenu.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// This is a basic entry in a context menu. It has a label and room for some sort of icon on the left.
|
||||
/// If this entry has a sub-menu, it also shows a little ">" icon on the right.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public partial class ContextMenuElement : ContainerButton
|
||||
{
|
||||
public const string StyleClassContextMenuButton = "contextMenuButton";
|
||||
|
||||
public const float ElementMargin = 2;
|
||||
public const float ElementHeight = 32;
|
||||
|
||||
/// <summary>
|
||||
/// The menu that contains this element
|
||||
/// </summary>
|
||||
public ContextMenuPopup? ParentMenu;
|
||||
|
||||
private ContextMenuPopup? _subMenu;
|
||||
|
||||
/// <summary>
|
||||
/// The pop-up menu that is opened when hovering over this element.
|
||||
/// </summary>
|
||||
public ContextMenuPopup? SubMenu
|
||||
{
|
||||
get => _subMenu;
|
||||
set
|
||||
{
|
||||
_subMenu = value;
|
||||
ExpansionIndicator.Visible = _subMenu != null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience property to set label text.
|
||||
/// </summary>
|
||||
public string Text { set => Label.SetMessage(FormattedMessage.FromMarkupPermissive(value.Trim())); }
|
||||
|
||||
public ContextMenuElement(string? text = null)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
Margin = new Thickness(ElementMargin, ElementMargin, ElementMargin, ElementMargin);
|
||||
SetOnlyStyleClass(StyleClassContextMenuButton);
|
||||
|
||||
if (text != null)
|
||||
Text = text;
|
||||
|
||||
ExpansionIndicator.Texture = IoCManager.Resolve<IResourceCache>()
|
||||
.GetTexture("/Textures/Interface/VerbIcons/group.svg.192dpi.png");
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
_subMenu?.Dispose();
|
||||
_subMenu = null;
|
||||
ParentMenu = null;
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
UpdateStyle();
|
||||
base.Draw(handle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If this element's sub-menu is currently visible, give it the hovered pseudo class.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Basically: if we are in a sub menu, keep the element in the parent menu highlighted even though we are
|
||||
/// not actually hovering over it.
|
||||
/// </remarks>
|
||||
protected virtual void UpdateStyle()
|
||||
{
|
||||
if ((_subMenu?.Visible ?? false) && !HasStylePseudoClass(StylePseudoClassHover))
|
||||
{
|
||||
AddStylePseudoClass(StylePseudoClassHover);
|
||||
return;
|
||||
}
|
||||
|
||||
if (DrawMode == DrawModeEnum.Hover)
|
||||
return;
|
||||
|
||||
if (_subMenu?.Visible ?? true)
|
||||
return;
|
||||
|
||||
RemoveStylePseudoClass(StylePseudoClassHover);
|
||||
}
|
||||
}
|
||||
}
|
||||
6
Content.Client/ContextMenu/UI/ContextMenuPopup.xaml
Normal file
6
Content.Client/ContextMenu/UI/ContextMenuPopup.xaml
Normal file
@@ -0,0 +1,6 @@
|
||||
<Popup xmlns="https://spacestation14.io">
|
||||
<PanelContainer Name="MenuPanel">
|
||||
<ScrollContainer Name="Scroll" VerticalExpand="True" HScrollEnabled="False" ReturnMeasure="True">
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
</Popup>
|
||||
70
Content.Client/ContextMenu/UI/ContextMenuPopup.xaml.cs
Normal file
70
Content.Client/ContextMenu/UI/ContextMenuPopup.xaml.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Content.Client.ContextMenu.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// The base context-menu pop-up window used for both the entity and verb menus.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public partial class ContextMenuPopup : Popup
|
||||
{
|
||||
public const string StyleClassContextMenuPopup = "contextMenuPopup";
|
||||
|
||||
/// <summary>
|
||||
/// How many items to list before limiting the size and adding a scroll bar.
|
||||
/// </summary>
|
||||
public const int MaxItemsBeforeScroll = 10;
|
||||
|
||||
/// <summary>
|
||||
/// If this pop-up is created by hovering over some element in another pop-up, this is that element.
|
||||
/// </summary>
|
||||
public ContextMenuElement? ParentElement;
|
||||
|
||||
/// <summary>
|
||||
/// This is the main body of the menu. The menu entries should be added to this object.
|
||||
/// </summary>
|
||||
public BoxContainer MenuBody = new() { Orientation = LayoutOrientation.Vertical };
|
||||
|
||||
private ContextMenuPresenter _presenter;
|
||||
|
||||
public ContextMenuPopup (ContextMenuPresenter presenter, ContextMenuElement? parentElement) : base()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
MenuPanel.SetOnlyStyleClass(StyleClassContextMenuPopup);
|
||||
|
||||
_presenter = presenter;
|
||||
ParentElement = parentElement;
|
||||
|
||||
//XAML controls are private. So defining and adding MenuBody here instead.
|
||||
Scroll.AddChild(MenuBody);
|
||||
|
||||
// Set Max Height based on MaxItemsBeforeScroll and the panel's style box
|
||||
MenuPanel.ForceRunStyleUpdate();
|
||||
MenuPanel.TryGetStyleProperty<StyleBox>(PanelContainer.StylePropertyPanel, out var box);
|
||||
var styleSize = (box?.MinimumSize ?? Vector2.Zero) / UIScale;
|
||||
MenuPanel.MaxHeight = MaxItemsBeforeScroll * (ContextMenuElement.ElementHeight + 2 * ContextMenuElement.ElementMargin) + styleSize.Y;
|
||||
|
||||
UserInterfaceManager.ModalRoot.AddChild(this);
|
||||
MenuBody.OnChildRemoved += ctrl => _presenter.OnRemoveElement(this, ctrl);
|
||||
|
||||
if (ParentElement != null)
|
||||
{
|
||||
DebugTools.Assert(ParentElement.SubMenu == null);
|
||||
ParentElement.SubMenu = this;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
MenuBody.OnChildRemoved -= ctrl => _presenter.OnRemoveElement(this, ctrl);
|
||||
ParentElement = null;
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,389 +1,179 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Content.Client.Examine;
|
||||
using Content.Client.Interactable;
|
||||
using Content.Client.Items.Managers;
|
||||
using Content.Client.Verbs;
|
||||
using Content.Client.Viewport;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Input;
|
||||
using Content.Shared.Interaction.Helpers;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Timing;
|
||||
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
|
||||
using Robust.Shared.Maths;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
namespace Content.Client.ContextMenu.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// This class handles all the logic associated with showing a context menu.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This largely involves setting up timers to open and close sub-menus when hovering over other menu elements.
|
||||
/// </remarks>
|
||||
public class ContextMenuPresenter : IDisposable
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
|
||||
[Dependency] private readonly IItemSlotManager _itemSlotManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IStateManager _stateManager = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
|
||||
public static readonly TimeSpan HoverDelay = TimeSpan.FromSeconds(0.2);
|
||||
private CancellationTokenSource? _cancelHover;
|
||||
|
||||
private readonly IContextMenuView _contextMenuView;
|
||||
private readonly VerbSystem _verbSystem;
|
||||
public ContextMenuPopup RootMenu;
|
||||
public Stack<ContextMenuPopup> Menus { get; } = new();
|
||||
|
||||
private MapCoordinates _mapCoordinates;
|
||||
/// <summary>
|
||||
/// Used to cancel the timer that opens menus.
|
||||
/// </summary>
|
||||
public CancellationTokenSource? CancelOpen;
|
||||
|
||||
public ContextMenuPresenter(VerbSystem verbSystem)
|
||||
/// <summary>
|
||||
/// Used to cancel the timer that closes menus.
|
||||
/// </summary>
|
||||
public CancellationTokenSource? CancelClose;
|
||||
|
||||
public ContextMenuPresenter()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_verbSystem = verbSystem;
|
||||
|
||||
_contextMenuView = new ContextMenuView();
|
||||
_contextMenuView.OnKeyBindDownSingle += OnKeyBindDownSingle;
|
||||
_contextMenuView.OnMouseEnteredSingle += OnMouseEnteredSingle;
|
||||
_contextMenuView.OnMouseExitedSingle += OnMouseExitedSingle;
|
||||
_contextMenuView.OnMouseHoveringSingle += OnMouseHoveringSingle;
|
||||
|
||||
_contextMenuView.OnKeyBindDownStack += OnKeyBindDownStack;
|
||||
_contextMenuView.OnMouseEnteredStack += OnMouseEnteredStack;
|
||||
|
||||
_contextMenuView.OnExitedTree += OnExitedTree;
|
||||
_contextMenuView.OnCloseRootMenu += OnCloseRootMenu;
|
||||
_contextMenuView.OnCloseChildMenu += OnCloseChildMenu;
|
||||
|
||||
_cfg.OnValueChanged(CCVars.ContextMenuGroupingType, _contextMenuView.OnGroupingContextMenuChanged, true);
|
||||
|
||||
CommandBinds.Builder
|
||||
.Bind(ContentKeyFunctions.OpenContextMenu, new PointerInputCmdHandler(HandleOpenContextMenu))
|
||||
.Register<ContextMenuPresenter>();
|
||||
}
|
||||
|
||||
#region View Events
|
||||
private void OnCloseChildMenu(object? sender, int depth)
|
||||
{
|
||||
_contextMenuView.CloseContextPopups(depth);
|
||||
}
|
||||
|
||||
private void OnCloseRootMenu(object? sender, EventArgs e)
|
||||
{
|
||||
_contextMenuView.CloseContextPopups();
|
||||
}
|
||||
|
||||
private void OnExitedTree(object? sender, ContextMenuElement e)
|
||||
{
|
||||
_contextMenuView.UpdateParents(e);
|
||||
}
|
||||
|
||||
private void OnMouseEnteredStack(object? sender, StackContextElement e)
|
||||
{
|
||||
var realGlobalPosition = e.GlobalPosition;
|
||||
|
||||
_cancelHover?.Cancel();
|
||||
_cancelHover = new();
|
||||
|
||||
Timer.Spawn(HoverDelay, () =>
|
||||
{
|
||||
if (_contextMenuView.Menus.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnCloseChildMenu(sender, e.ParentMenu?.Depth ?? 0);
|
||||
|
||||
var filteredEntities = e.ContextEntities.Where(entity => !entity.Deleted);
|
||||
if (filteredEntities.Any())
|
||||
{
|
||||
_contextMenuView.AddChildMenu(filteredEntities, realGlobalPosition, e);
|
||||
}
|
||||
}, _cancelHover.Token);
|
||||
}
|
||||
|
||||
private void OnKeyBindDownStack(object? sender, (GUIBoundKeyEventArgs, StackContextElement) e)
|
||||
{
|
||||
var (args, stack) = e;
|
||||
var firstEntity = stack.ContextEntities.FirstOrDefault(ent => !ent.Deleted);
|
||||
|
||||
if (firstEntity == null) return;
|
||||
|
||||
if (args.Function == EngineKeyFunctions.Use || args.Function == ContentKeyFunctions.AltActivateItemInWorld || args.Function == ContentKeyFunctions.TryPullObject || args.Function == ContentKeyFunctions.MovePulledObject)
|
||||
{
|
||||
var inputSys = _systemManager.GetEntitySystem<InputSystem>();
|
||||
|
||||
var func = args.Function;
|
||||
var funcId = _inputManager.NetworkBindMap.KeyFunctionID(func);
|
||||
|
||||
var message = new FullInputCmdMessage(_gameTiming.CurTick, _gameTiming.TickFraction, funcId,
|
||||
BoundKeyState.Down, firstEntity.Transform.Coordinates, args.PointerLocation, firstEntity.Uid);
|
||||
|
||||
var session = _playerManager.LocalPlayer?.Session;
|
||||
if (session != null)
|
||||
{
|
||||
inputSys.HandleInputCommand(session, func, message);
|
||||
}
|
||||
CloseAllMenus();
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_itemSlotManager.OnButtonPressed(args, firstEntity))
|
||||
{
|
||||
CloseAllMenus();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMouseHoveringSingle(object? sender, SingleContextElement e)
|
||||
{
|
||||
if (!e.DrawOutline) return;
|
||||
|
||||
var localPlayer = _playerManager.LocalPlayer;
|
||||
if (localPlayer?.ControlledEntity != null)
|
||||
{
|
||||
var inRange =
|
||||
localPlayer.InRangeUnobstructed(e.ContextEntity, ignoreInsideBlocker: true);
|
||||
|
||||
// BUG: This assumes that the main viewport is the viewport that the context menu is active on.
|
||||
// This is not necessarily true but we currently have no way to find the viewport (reliably)
|
||||
// from the input event.
|
||||
//
|
||||
// This might be particularly important in the future with a more advanced mapping mode.
|
||||
var renderScale = _eyeManager.MainViewport.GetRenderScale();
|
||||
e.OutlineComponent?.UpdateInRange(inRange, renderScale);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMouseEnteredSingle(object? sender, SingleContextElement e)
|
||||
{
|
||||
// close other pop-ups after a short delay
|
||||
_cancelHover?.Cancel();
|
||||
_cancelHover = new();
|
||||
|
||||
Timer.Spawn(HoverDelay, () =>
|
||||
{
|
||||
if (_contextMenuView.Menus.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnCloseChildMenu(sender, e.ParentMenu?.Depth ?? 0);
|
||||
|
||||
}, _cancelHover.Token);
|
||||
|
||||
|
||||
var entity = e.ContextEntity;
|
||||
|
||||
OnCloseChildMenu(sender, e.ParentMenu?.Depth ?? 0);
|
||||
|
||||
if (entity.Deleted) return;
|
||||
|
||||
var localPlayer = _playerManager.LocalPlayer;
|
||||
if (localPlayer?.ControlledEntity == null) return;
|
||||
|
||||
var renderScale = _eyeManager.MainViewport.GetRenderScale();
|
||||
e.OutlineComponent?.OnMouseEnter(localPlayer.InRangeUnobstructed(entity, ignoreInsideBlocker: true), renderScale);
|
||||
if (e.SpriteComp != null)
|
||||
{
|
||||
e.SpriteComp.DrawDepth = (int) DrawDepth.HighlightedItems;
|
||||
}
|
||||
e.DrawOutline = true;
|
||||
}
|
||||
|
||||
private void OnMouseExitedSingle(object? sender, SingleContextElement e)
|
||||
{
|
||||
if (!e.ContextEntity.Deleted)
|
||||
{
|
||||
if (e.SpriteComp != null)
|
||||
{
|
||||
e.SpriteComp.DrawDepth = e.OriginalDrawDepth;
|
||||
}
|
||||
e.OutlineComponent?.OnMouseLeave();
|
||||
}
|
||||
e.DrawOutline = false;
|
||||
}
|
||||
|
||||
private void OnKeyBindDownSingle(object? sender, (GUIBoundKeyEventArgs, SingleContextElement) valueTuple)
|
||||
{
|
||||
var (args, single) = valueTuple;
|
||||
var entity = single.ContextEntity;
|
||||
if (args.Function == ContentKeyFunctions.OpenContextMenu)
|
||||
{
|
||||
_verbSystem.OnContextButtonPressed(entity);
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Function == ContentKeyFunctions.ExamineEntity)
|
||||
{
|
||||
_systemManager.GetEntitySystem<ExamineSystem>().DoExamine(entity);
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Function == EngineKeyFunctions.Use || args.Function == ContentKeyFunctions.AltActivateItemInWorld || args.Function == ContentKeyFunctions.Point ||
|
||||
args.Function == ContentKeyFunctions.TryPullObject || args.Function == ContentKeyFunctions.MovePulledObject)
|
||||
{
|
||||
var inputSys = _systemManager.GetEntitySystem<InputSystem>();
|
||||
|
||||
var func = args.Function;
|
||||
var funcId = _inputManager.NetworkBindMap.KeyFunctionID(func);
|
||||
|
||||
var message = new FullInputCmdMessage(_gameTiming.CurTick, _gameTiming.TickFraction, funcId,
|
||||
BoundKeyState.Down, entity.Transform.Coordinates, args.PointerLocation, entity.Uid);
|
||||
|
||||
var session = _playerManager.LocalPlayer?.Session;
|
||||
if (session != null)
|
||||
{
|
||||
inputSys.HandleInputCommand(session, func, message);
|
||||
}
|
||||
|
||||
CloseAllMenus();
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_itemSlotManager.OnButtonPressed(args, single.ContextEntity))
|
||||
{
|
||||
CloseAllMenus();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Model Updates
|
||||
private bool HandleOpenContextMenu(in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
if (args.State != BoundKeyState.Down)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_stateManager.CurrentState is not GameScreenBase)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var player = _playerManager.LocalPlayer?.ControlledEntity;
|
||||
if (player == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_mapCoordinates = args.Coordinates.ToMap(_entityManager);
|
||||
|
||||
if (!_verbSystem.TryGetContextEntities(player, _mapCoordinates, out var entities, ignoreVisibility: _verbSystem.CanSeeAllContext))
|
||||
return false;
|
||||
|
||||
// do we need to do visiblity checks?
|
||||
if (_verbSystem.CanSeeAllContext)
|
||||
{
|
||||
_contextMenuView.AddRootMenu(entities);
|
||||
return true;
|
||||
}
|
||||
|
||||
//visibility checks
|
||||
player.TryGetContainer(out var playerContainer);
|
||||
foreach (var entity in entities.ToList())
|
||||
{
|
||||
if (!entity.TryGetComponent(out ISpriteComponent? spriteComponent) ||
|
||||
!spriteComponent.Visible ||
|
||||
!CanSeeContainerCheck(entity, playerContainer))
|
||||
{
|
||||
entities.Remove(entity);
|
||||
}
|
||||
}
|
||||
|
||||
if (entities.Count == 0)
|
||||
return false;
|
||||
|
||||
_contextMenuView.AddRootMenu(entities);
|
||||
return true;
|
||||
RootMenu = new(this, null);
|
||||
RootMenu.OnPopupHide += RootMenu.MenuBody.DisposeAllChildren;
|
||||
Menus.Push(RootMenu);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can the player see the entity through any entity containers?
|
||||
/// Dispose of all UI elements.
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
RootMenu.OnPopupHide -= RootMenu.MenuBody.DisposeAllChildren;
|
||||
RootMenu.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Close and clear the root menu. This will also dispose any sub-menus.
|
||||
/// </summary>
|
||||
public virtual void Close()
|
||||
{
|
||||
RootMenu.Close();
|
||||
CancelOpen?.Cancel();
|
||||
CancelClose?.Cancel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts closing menus until the top-most menu is the given one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is similar to <see cref="ContainerHelpers.IsInSameOrParentContainer()"/>, except that we do not
|
||||
/// allow the player to be the "parent" container and we allow for see-through containers (display cases).
|
||||
/// Note that this does not actually check if the given menu IS a sub menu of this presenter. In that case
|
||||
/// this will close all menus.
|
||||
/// </remarks>
|
||||
private bool CanSeeContainerCheck(IEntity entity, IContainer? playerContainer)
|
||||
public void CloseSubMenus(ContextMenuPopup? menu)
|
||||
{
|
||||
// is the player inside this entity?
|
||||
if (playerContainer?.Owner == entity)
|
||||
return true;
|
||||
if (menu == null || !menu.Visible)
|
||||
return;
|
||||
|
||||
entity.TryGetContainer(out var entityContainer);
|
||||
|
||||
// are they in the same container (or none?)
|
||||
if (playerContainer == entityContainer)
|
||||
return true;
|
||||
|
||||
// Is the entity in a display case?
|
||||
if (playerContainer == null && entityContainer!.ShowContents)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
while (Menus.TryPeek(out var subMenu) && subMenu != menu)
|
||||
{
|
||||
Menus.Pop().Close();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check that entities in the context menu are still visible. If not, remove them from the context menu.
|
||||
/// Start a timer to open this element's sub-menu.
|
||||
/// </summary>
|
||||
public void Update()
|
||||
public virtual void OnMouseEntered(ContextMenuElement element)
|
||||
{
|
||||
if (_contextMenuView.Elements.Count == 0)
|
||||
var topMenu = Menus.Peek();
|
||||
|
||||
if (element.ParentMenu == topMenu || element.SubMenu == topMenu)
|
||||
CancelClose?.Cancel();
|
||||
|
||||
if (element.SubMenu == topMenu)
|
||||
return;
|
||||
|
||||
var player = _playerManager.LocalPlayer?.ControlledEntity;
|
||||
// open the sub-menu after a short delay.
|
||||
CancelOpen?.Cancel();
|
||||
CancelOpen = new();
|
||||
Timer.Spawn(HoverDelay, () => OpenSubMenu(element), CancelOpen.Token);
|
||||
}
|
||||
|
||||
if (player == null)
|
||||
/// <summary>
|
||||
/// Start a timer to close this element's sub-menu.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that this timer will be aborted when entering the actual sub-menu itself.
|
||||
/// </remarks>
|
||||
public virtual void OnMouseExited(ContextMenuElement element)
|
||||
{
|
||||
CancelOpen?.Cancel();
|
||||
|
||||
if (element.SubMenu == null)
|
||||
return;
|
||||
|
||||
foreach (var entity in _contextMenuView.Elements.Keys.ToList())
|
||||
{
|
||||
if (entity.Deleted || !_verbSystem.CanSeeAllContext && !player.InRangeUnOccluded(entity))
|
||||
{
|
||||
_contextMenuView.RemoveEntity(entity);
|
||||
if (_verbSystem.CurrentTarget == entity.Uid)
|
||||
_verbSystem.CloseVerbMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
public void CloseAllMenus()
|
||||
{
|
||||
_contextMenuView.CloseContextPopups();
|
||||
_verbSystem.CloseVerbMenu();
|
||||
CancelClose?.Cancel();
|
||||
CancelClose = new();
|
||||
Timer.Spawn(HoverDelay, () => CloseSubMenus(element.ParentMenu), CancelClose.Token);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
public virtual void OnKeyBindDown(ContextMenuElement element, GUIBoundKeyEventArgs args) { }
|
||||
|
||||
/// <summary>
|
||||
/// Opens a new sub menu, and close the old one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the given element has no sub-menu, just close the current one.
|
||||
/// </remarks>
|
||||
public virtual void OpenSubMenu(ContextMenuElement element)
|
||||
{
|
||||
_contextMenuView.OnKeyBindDownSingle -= OnKeyBindDownSingle;
|
||||
_contextMenuView.OnMouseEnteredSingle -= OnMouseEnteredSingle;
|
||||
_contextMenuView.OnMouseExitedSingle -= OnMouseExitedSingle;
|
||||
_contextMenuView.OnMouseHoveringSingle -= OnMouseHoveringSingle;
|
||||
// If This is already the top most menu, do nothing.
|
||||
if (element.SubMenu == Menus.Peek())
|
||||
return;
|
||||
|
||||
_contextMenuView.OnKeyBindDownStack -= OnKeyBindDownStack;
|
||||
_contextMenuView.OnMouseEnteredStack -= OnMouseEnteredStack;
|
||||
// Was the parent menu closed or disposed before an open timer completed?
|
||||
if (element.Disposed || element.ParentMenu == null || !element.ParentMenu.Visible)
|
||||
return;
|
||||
|
||||
_contextMenuView.OnExitedTree -= OnExitedTree;
|
||||
_contextMenuView.OnCloseRootMenu -= OnCloseRootMenu;
|
||||
_contextMenuView.OnCloseChildMenu -= OnCloseChildMenu;
|
||||
// Close any currently open sub-menus up to this element's parent menu.
|
||||
CloseSubMenus(element.ParentMenu);
|
||||
|
||||
CommandBinds.Unregister<ContextMenuPresenter>();
|
||||
if (element.SubMenu == null)
|
||||
return;
|
||||
|
||||
// open pop-up adjacent to the parent element. We want the sub-menu elements to align with this element
|
||||
// which depends on the panel container style margins.
|
||||
var altPos = element.GlobalPosition;
|
||||
var pos = altPos + (element.Width + 2*ContextMenuElement.ElementMargin, - 2*ContextMenuElement.ElementMargin);
|
||||
element.SubMenu.Open(UIBox2.FromDimensions(pos, (1, 1)), altPos);
|
||||
element.SubMenu.Close();
|
||||
element.SubMenu.Open(UIBox2.FromDimensions(pos, (1, 1)), altPos);
|
||||
|
||||
// draw on top of other menus
|
||||
element.SubMenu.SetPositionLast();
|
||||
|
||||
Menus.Push(element.SubMenu);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an element to a menu and subscribe to GUI events.
|
||||
/// </summary>
|
||||
public void AddElement(ContextMenuPopup menu, ContextMenuElement element)
|
||||
{
|
||||
element.OnMouseEntered += _ => OnMouseEntered(element);
|
||||
element.OnMouseExited += _ => OnMouseExited(element);
|
||||
element.OnKeyBindDown += args => OnKeyBindDown(element, args);
|
||||
element.ParentMenu = menu;
|
||||
menu.MenuBody.AddChild(element);
|
||||
menu.InvalidateMeasure();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes event subscriptions when an element is removed from a menu,
|
||||
/// </summary>
|
||||
public void OnRemoveElement(ContextMenuPopup menu, Control control)
|
||||
{
|
||||
if (control is not ContextMenuElement element)
|
||||
return;
|
||||
|
||||
element.OnMouseEntered -= _ => OnMouseEntered(element);
|
||||
element.OnMouseExited -= _ => OnMouseExited(element);
|
||||
element.OnKeyBindDown -= args => OnKeyBindDown(element, args);
|
||||
|
||||
menu.InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.Client.ContextMenu.UI
|
||||
{
|
||||
public interface IContextMenuView : IDisposable
|
||||
{
|
||||
Dictionary<IEntity, ContextMenuElement> Elements { get; set; }
|
||||
Stack<ContextMenuPopup> Menus { get; }
|
||||
event EventHandler<(GUIBoundKeyEventArgs, SingleContextElement)>? OnKeyBindDownSingle;
|
||||
event EventHandler<SingleContextElement>? OnMouseEnteredSingle;
|
||||
event EventHandler<SingleContextElement>? OnMouseExitedSingle;
|
||||
event EventHandler<SingleContextElement>? OnMouseHoveringSingle;
|
||||
|
||||
event EventHandler<(GUIBoundKeyEventArgs, StackContextElement)>? OnKeyBindDownStack;
|
||||
event EventHandler<StackContextElement>? OnMouseEnteredStack;
|
||||
|
||||
event EventHandler<ContextMenuElement>? OnExitedTree;
|
||||
|
||||
event EventHandler? OnCloseRootMenu;
|
||||
event EventHandler<int>? OnCloseChildMenu;
|
||||
|
||||
void UpdateParents(ContextMenuElement element);
|
||||
void RemoveEntity(IEntity element);
|
||||
void AddRootMenu(List<IEntity> entities);
|
||||
void AddChildMenu(IEnumerable<IEntity> entities, Vector2 position, StackContextElement? stack);
|
||||
void CloseContextPopups(int depth);
|
||||
void CloseContextPopups();
|
||||
|
||||
void OnGroupingContextMenuChanged(int obj);
|
||||
}
|
||||
|
||||
public partial class ContextMenuView : IContextMenuView
|
||||
{
|
||||
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
|
||||
|
||||
public Stack<ContextMenuPopup> Menus { get; }
|
||||
public Dictionary<IEntity, ContextMenuElement> Elements { get; set; }
|
||||
|
||||
public event EventHandler<(GUIBoundKeyEventArgs, SingleContextElement)>? OnKeyBindDownSingle;
|
||||
public event EventHandler<SingleContextElement>? OnMouseEnteredSingle;
|
||||
public event EventHandler<SingleContextElement>? OnMouseExitedSingle;
|
||||
public event EventHandler<SingleContextElement>? OnMouseHoveringSingle;
|
||||
|
||||
public event EventHandler<(GUIBoundKeyEventArgs, StackContextElement)>? OnKeyBindDownStack;
|
||||
public event EventHandler<StackContextElement>? OnMouseEnteredStack;
|
||||
|
||||
public event EventHandler<ContextMenuElement>? OnExitedTree;
|
||||
|
||||
public event EventHandler? OnCloseRootMenu;
|
||||
public event EventHandler<int>? OnCloseChildMenu;
|
||||
|
||||
public ContextMenuView()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
Menus = new Stack<ContextMenuPopup>();
|
||||
Elements = new Dictionary<IEntity, ContextMenuElement>();
|
||||
}
|
||||
|
||||
public void AddRootMenu(List<IEntity> entities)
|
||||
{
|
||||
Elements = new Dictionary<IEntity, ContextMenuElement>(entities.Count);
|
||||
|
||||
var rootContextMenu = new ContextMenuPopup();
|
||||
rootContextMenu.OnPopupHide += () => OnCloseRootMenu?.Invoke(this, EventArgs.Empty);
|
||||
Menus.Push(rootContextMenu);
|
||||
|
||||
var entitySpriteStates = GroupEntities(entities);
|
||||
var orderedStates = entitySpriteStates.ToList();
|
||||
orderedStates.Sort((x, y) => string.CompareOrdinal(x.First().Prototype?.Name, y.First().Prototype?.Name));
|
||||
AddToUI(orderedStates);
|
||||
|
||||
_userInterfaceManager.ModalRoot.AddChild(rootContextMenu);
|
||||
var size = rootContextMenu.List.DesiredSize;
|
||||
var box = UIBox2.FromDimensions(_userInterfaceManager.MousePositionScaled.Position, size);
|
||||
rootContextMenu.Open(box);
|
||||
}
|
||||
public void AddChildMenu(IEnumerable<IEntity> entities, Vector2 position, StackContextElement? stack)
|
||||
{
|
||||
if (stack == null) return;
|
||||
var newDepth = stack.ParentMenu?.Depth + 1 ?? 1;
|
||||
var childContextMenu = new ContextMenuPopup(newDepth);
|
||||
Menus.Push(childContextMenu);
|
||||
|
||||
var orderedStates = GroupEntities(entities, newDepth);
|
||||
AddToUI(orderedStates, stack);
|
||||
|
||||
_userInterfaceManager.ModalRoot.AddChild(childContextMenu);
|
||||
var size = childContextMenu.List.DesiredSize;
|
||||
childContextMenu.Open(UIBox2.FromDimensions(position + (stack.Width, 0), size));
|
||||
}
|
||||
|
||||
private void AddToUI(List<List<IEntity>> entities, StackContextElement? stack = null)
|
||||
{
|
||||
if (entities.Count == 1)
|
||||
{
|
||||
foreach (var entity in entities[0])
|
||||
{
|
||||
AddSingleContextElement(entity, stack);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
if (entity.Count == 1)
|
||||
{
|
||||
AddSingleContextElement(entity[0], stack);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddStackContextElement(entity, stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private void AddSingleContextElement(IEntity entity, StackContextElement? pre)
|
||||
{
|
||||
if (Menus.TryPeek(out var menu))
|
||||
{
|
||||
var single = new SingleContextElement(entity, pre, menu);
|
||||
|
||||
single.OnKeyBindDown += args => OnKeyBindDownSingle?.Invoke(this, (args, single));
|
||||
single.OnMouseEntered += _ => OnMouseEnteredSingle?.Invoke(this, single);
|
||||
single.OnMouseExited += _ => OnMouseExitedSingle?.Invoke(this, single);
|
||||
single.OnMouseHovering += () => OnMouseHoveringSingle?.Invoke(this, single);
|
||||
single.OnExitedTree += () => OnExitedTree?.Invoke(this, single);
|
||||
|
||||
UpdateElements(entity, single);
|
||||
menu.AddToMenu(single);
|
||||
}
|
||||
}
|
||||
private void AddStackContextElement(IEnumerable<IEntity> entities, StackContextElement? pre)
|
||||
{
|
||||
if (Menus.TryPeek(out var menu))
|
||||
{
|
||||
var stack = new StackContextElement(entities, pre, menu);
|
||||
|
||||
stack.OnKeyBindDown += args => OnKeyBindDownStack?.Invoke(this, (args, stack));
|
||||
stack.OnMouseEntered += _ => OnMouseEnteredStack?.Invoke(this, stack);
|
||||
stack.OnExitedTree += () => OnExitedTree?.Invoke(this, stack);
|
||||
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
UpdateElements(entity, stack);
|
||||
}
|
||||
menu.AddToMenu(stack);
|
||||
}
|
||||
}
|
||||
private void UpdateElements(IEntity entity, ContextMenuElement element)
|
||||
{
|
||||
if (Elements.ContainsKey(entity))
|
||||
{
|
||||
Elements[entity] = element;
|
||||
}
|
||||
else
|
||||
{
|
||||
Elements.Add(entity, element);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveFromUI(ContextMenuElement element)
|
||||
{
|
||||
var menu = element.ParentMenu;
|
||||
if (menu != null)
|
||||
{
|
||||
menu.RemoveFromMenu(element);
|
||||
if (menu.List.ChildCount == 0)
|
||||
{
|
||||
OnCloseChildMenu?.Invoke(this, menu.Depth - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
public void RemoveEntity(IEntity entity)
|
||||
{
|
||||
var element = Elements[entity];
|
||||
switch (element)
|
||||
{
|
||||
case SingleContextElement singleContextElement:
|
||||
RemoveFromUI(singleContextElement);
|
||||
UpdateBranch(entity, singleContextElement.Pre);
|
||||
break;
|
||||
case StackContextElement stackContextElement:
|
||||
stackContextElement.RemoveEntity(entity);
|
||||
if (stackContextElement.EntitiesCount == 0)
|
||||
{
|
||||
RemoveFromUI(stackContextElement);
|
||||
}
|
||||
UpdateBranch(entity, stackContextElement.Pre);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(element));
|
||||
}
|
||||
Elements.Remove(entity);
|
||||
}
|
||||
private void UpdateBranch(IEntity entity, StackContextElement? stack)
|
||||
{
|
||||
while (stack != null)
|
||||
{
|
||||
stack.RemoveEntity(entity);
|
||||
if (stack.EntitiesCount == 0)
|
||||
{
|
||||
RemoveFromUI(stack);
|
||||
}
|
||||
|
||||
stack = stack.Pre;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateParents(ContextMenuElement element)
|
||||
{
|
||||
switch (element)
|
||||
{
|
||||
case SingleContextElement singleContextElement:
|
||||
if (singleContextElement.Pre != null)
|
||||
{
|
||||
Elements[singleContextElement.ContextEntity] = singleContextElement.Pre;
|
||||
}
|
||||
|
||||
break;
|
||||
case StackContextElement stackContextElement:
|
||||
if (stackContextElement.Pre != null)
|
||||
{
|
||||
foreach (var entity in stackContextElement.ContextEntities)
|
||||
{
|
||||
Elements[entity] = stackContextElement.Pre;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(element));
|
||||
}
|
||||
}
|
||||
|
||||
public void CloseContextPopups()
|
||||
{
|
||||
while (Menus.Count > 0)
|
||||
{
|
||||
Menus.Pop().Dispose();
|
||||
}
|
||||
|
||||
Elements.Clear();
|
||||
}
|
||||
public void CloseContextPopups(int depth)
|
||||
{
|
||||
while (Menus.Count > 0 && Menus.Peek().Depth > depth)
|
||||
{
|
||||
Menus.Pop().Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CloseContextPopups();
|
||||
}
|
||||
}
|
||||
}
|
||||
70
Content.Client/ContextMenu/UI/EntityMenuElement.cs
Normal file
70
Content.Client/ContextMenu/UI/EntityMenuElement.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using Content.Client.Stylesheets;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.Client.ContextMenu.UI
|
||||
{
|
||||
public partial class EntityMenuElement : ContextMenuElement
|
||||
{
|
||||
public const string StyleClassEntityMenuCountText = "contextMenuCount";
|
||||
|
||||
/// <summary>
|
||||
/// The entity that can be accessed by interacting with this element.
|
||||
/// </summary>
|
||||
public IEntity? Entity;
|
||||
|
||||
/// <summary>
|
||||
/// How many entities are accessible through this element's sub-menus.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used for <see cref="CountLabel"/>
|
||||
/// </remarks>
|
||||
public int Count;
|
||||
|
||||
public Label CountLabel;
|
||||
public SpriteView EntityIcon = new SpriteView { OverrideDirection = Direction.South};
|
||||
|
||||
public EntityMenuElement(IEntity? entity = null) : base()
|
||||
{
|
||||
CountLabel = new Label { StyleClasses = { StyleClassEntityMenuCountText } };
|
||||
Icon.AddChild(new LayoutContainer() { Children = { EntityIcon, CountLabel } });
|
||||
|
||||
LayoutContainer.SetAnchorPreset(CountLabel, LayoutContainer.LayoutPreset.BottomRight);
|
||||
LayoutContainer.SetGrowHorizontal(CountLabel, LayoutContainer.GrowDirection.Begin);
|
||||
LayoutContainer.SetGrowVertical(CountLabel, LayoutContainer.GrowDirection.Begin);
|
||||
|
||||
Entity = entity;
|
||||
if (Entity != null)
|
||||
{
|
||||
Count = 1;
|
||||
CountLabel.Visible = false;
|
||||
UpdateEntity();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
Entity = null;
|
||||
Count = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the icon and text of this element based on the given entity or this element's own entity if none
|
||||
/// is provided.
|
||||
/// </summary>
|
||||
public void UpdateEntity(IEntity? entity = null)
|
||||
{
|
||||
entity ??= Entity;
|
||||
|
||||
EntityIcon.Sprite = entity?.GetComponentOrNull<ISpriteComponent>();
|
||||
|
||||
if (UserInterfaceManager.DebugMonitors.Visible)
|
||||
Text = $"{entity?.Name} ({entity?.Uid})";
|
||||
else
|
||||
Text = entity?.Name ?? string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
358
Content.Client/ContextMenu/UI/EntityMenuPresenter.cs
Normal file
358
Content.Client/ContextMenu/UI/EntityMenuPresenter.cs
Normal file
@@ -0,0 +1,358 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Client.Examine;
|
||||
using Content.Client.Items.Managers;
|
||||
using Content.Client.Verbs;
|
||||
using Content.Client.Viewport;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Input;
|
||||
using Content.Shared.Interaction.Helpers;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
namespace Content.Client.ContextMenu.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// This class handles the displaying of the entity context menu.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In addition to the normal <see cref="ContextMenuPresenter"/> functionality, this also provides functions get
|
||||
/// a list of entities near the mouse position, add them to the context menu grouped by prototypes, and remove
|
||||
/// them from the menu as they move out of sight.
|
||||
/// </remarks>
|
||||
public sealed partial class EntityMenuPresenter : ContextMenuPresenter
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
|
||||
[Dependency] private readonly IItemSlotManager _itemSlotManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IStateManager _stateManager = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
|
||||
private readonly VerbSystem _verbSystem;
|
||||
|
||||
/// <summary>
|
||||
/// This maps the currently displayed entities to the actual GUI elements.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used remove GUI elements when the entities are deleted. or leave the LOS.
|
||||
/// </remarks>
|
||||
public Dictionary<IEntity, EntityMenuElement> Elements = new();
|
||||
|
||||
public EntityMenuPresenter(VerbSystem verbSystem) : base()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_verbSystem = verbSystem;
|
||||
|
||||
_cfg.OnValueChanged(CCVars.EntityMenuGroupingType, OnGroupingChanged, true);
|
||||
|
||||
CommandBinds.Builder
|
||||
.Bind(ContentKeyFunctions.OpenContextMenu, new PointerInputCmdHandler(HandleOpenEntityMenu))
|
||||
.Register<EntityMenuPresenter>();
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
Elements.Clear();
|
||||
CommandBinds.Unregister<EntityMenuPresenter>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a list of entities, sort them into groups and them to a new entity menu.
|
||||
/// </summary>
|
||||
public void OpenRootMenu(List<IEntity> entities)
|
||||
{
|
||||
var entitySpriteStates = GroupEntities(entities);
|
||||
var orderedStates = entitySpriteStates.ToList();
|
||||
orderedStates.Sort((x, y) => string.CompareOrdinal(x.First().Prototype?.Name, y.First().Prototype?.Name));
|
||||
Elements.Clear();
|
||||
AddToUI(orderedStates);
|
||||
|
||||
var box = UIBox2.FromDimensions(_userInterfaceManager.MousePositionScaled.Position, (1, 1));
|
||||
RootMenu.Open(box);
|
||||
}
|
||||
|
||||
public override void OnKeyBindDown(ContextMenuElement element, GUIBoundKeyEventArgs args)
|
||||
{
|
||||
base.OnKeyBindDown(element, args);
|
||||
if (element is not EntityMenuElement entityElement)
|
||||
return;
|
||||
|
||||
// get an entity associated with this element
|
||||
var entity = entityElement.Entity;
|
||||
entity ??= GetFirstEntityOrNull(element.SubMenu);
|
||||
if (entity == null)
|
||||
return;
|
||||
|
||||
// open verb menu?
|
||||
if (args.Function == ContentKeyFunctions.OpenContextMenu)
|
||||
{
|
||||
_verbSystem.VerbMenu.OpenVerbMenu(entity);
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
// do examination?
|
||||
if (args.Function == ContentKeyFunctions.ExamineEntity)
|
||||
{
|
||||
_systemManager.GetEntitySystem<ExamineSystem>().DoExamine(entity);
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
// do some other server-side interaction?
|
||||
if (args.Function == EngineKeyFunctions.Use || args.Function == ContentKeyFunctions.AltActivateItemInWorld || args.Function == ContentKeyFunctions.Point ||
|
||||
args.Function == ContentKeyFunctions.TryPullObject || args.Function == ContentKeyFunctions.MovePulledObject)
|
||||
{
|
||||
var inputSys = _systemManager.GetEntitySystem<InputSystem>();
|
||||
|
||||
var func = args.Function;
|
||||
var funcId = _inputManager.NetworkBindMap.KeyFunctionID(func);
|
||||
|
||||
var message = new FullInputCmdMessage(_gameTiming.CurTick, _gameTiming.TickFraction, funcId,
|
||||
BoundKeyState.Down, entity.Transform.Coordinates, args.PointerLocation, entity.Uid);
|
||||
|
||||
var session = _playerManager.LocalPlayer?.Session;
|
||||
if (session != null)
|
||||
{
|
||||
inputSys.HandleInputCommand(session, func, message);
|
||||
}
|
||||
|
||||
_verbSystem.CloseAllMenus();
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_itemSlotManager.OnButtonPressed(args, entity))
|
||||
{
|
||||
_verbSystem.CloseAllMenus();
|
||||
}
|
||||
}
|
||||
|
||||
private bool HandleOpenEntityMenu(in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
if (args.State != BoundKeyState.Down)
|
||||
return false;
|
||||
|
||||
if (_stateManager.CurrentState is not GameScreenBase)
|
||||
return false;
|
||||
|
||||
var coords = args.Coordinates.ToMap(_entityManager);
|
||||
|
||||
if (!_verbSystem.TryGetEntityMenuEntities(coords, out var entities))
|
||||
return false;
|
||||
|
||||
OpenRootMenu(entities);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check that entities in the context menu are still visible. If not, remove them from the context menu.
|
||||
/// </summary>
|
||||
public void Update()
|
||||
{
|
||||
if (!RootMenu.Visible)
|
||||
return;
|
||||
|
||||
var player = _playerManager.LocalPlayer?.ControlledEntity;
|
||||
|
||||
if (player == null)
|
||||
return;
|
||||
|
||||
// Do we need to do in-range unOccluded checks?
|
||||
var ignoreFov = !_eyeManager.CurrentEye.DrawFov ||
|
||||
(_verbSystem.Visibility & MenuVisibility.NoFov) == MenuVisibility.NoFov;
|
||||
|
||||
foreach (var entity in Elements.Keys.ToList())
|
||||
{
|
||||
if (entity.Deleted || !ignoreFov && !player.InRangeUnOccluded(entity))
|
||||
RemoveEntity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add menu elements for a list of grouped entities;
|
||||
/// </summary>
|
||||
/// <param name="entityGroups"> A list of entity groups. Entities are grouped together based on prototype.</param>
|
||||
private void AddToUI(List<List<IEntity>> entityGroups)
|
||||
{
|
||||
// If there is only a single group. We will just directly list individual entities
|
||||
if (entityGroups.Count == 1)
|
||||
{
|
||||
foreach (var entity in entityGroups[0])
|
||||
{
|
||||
var element = new EntityMenuElement(entity);
|
||||
AddElement(RootMenu, element);
|
||||
Elements.TryAdd(entity, element);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var group in entityGroups)
|
||||
{
|
||||
if (group.Count > 1)
|
||||
{
|
||||
AddGroupToUI(group);
|
||||
continue;
|
||||
}
|
||||
|
||||
// this group only has a single entity, add a simple menu element
|
||||
var element = new EntityMenuElement(group[0]);
|
||||
AddElement(RootMenu, element);
|
||||
Elements.TryAdd(group[0], element);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a group of entities, add a menu element that has a pop-up sub-menu listing group members
|
||||
/// </summary>
|
||||
private void AddGroupToUI(List<IEntity> group)
|
||||
{
|
||||
EntityMenuElement element = new();
|
||||
ContextMenuPopup subMenu = new(this, element);
|
||||
|
||||
foreach (var entity in group)
|
||||
{
|
||||
var subElement = new EntityMenuElement(entity);
|
||||
AddElement(subMenu, subElement);
|
||||
Elements.TryAdd(entity, subElement);
|
||||
}
|
||||
|
||||
UpdateElement(element);
|
||||
AddElement(RootMenu, element);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an entity from the entity context menu.
|
||||
/// </summary>
|
||||
private void RemoveEntity(IEntity entity)
|
||||
{
|
||||
// find the element associated with this entity
|
||||
if (!Elements.TryGetValue(entity, out var element))
|
||||
{
|
||||
Logger.Error($"Attempted to remove unknown entity from the entity menu: {entity.Name} ({entity.Uid})");
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the element
|
||||
var parent = element.ParentMenu?.ParentElement;
|
||||
element.Dispose();
|
||||
Elements.Remove(entity);
|
||||
|
||||
// update any parent elements
|
||||
if (parent is EntityMenuElement e)
|
||||
UpdateElement(e);
|
||||
|
||||
// if the verb menu is open and targeting this entity, close it.
|
||||
if (_verbSystem.VerbMenu.CurrentTarget == entity.Uid)
|
||||
_verbSystem.VerbMenu.Close();
|
||||
|
||||
// If this was the last entity, close the entity menu
|
||||
if (RootMenu.MenuBody.ChildCount == 0)
|
||||
Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the information displayed by a menu element.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is called when initializing elements or after an element was removed from a sub-menu.
|
||||
/// </remarks>
|
||||
private void UpdateElement(EntityMenuElement element)
|
||||
{
|
||||
if (element.SubMenu == null)
|
||||
return;
|
||||
|
||||
// Get the first entity in the sub-menus
|
||||
var entity = GetFirstEntityOrNull(element.SubMenu);
|
||||
if (entity == null)
|
||||
{
|
||||
// This whole element has no associated entities. We should remove it
|
||||
element.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
element.UpdateEntity(entity);
|
||||
|
||||
// Update the entity count & count label
|
||||
element.Count = 0;
|
||||
foreach (var subElement in element.SubMenu.MenuBody.Children)
|
||||
{
|
||||
if (subElement is EntityMenuElement entityElement)
|
||||
element.Count += entityElement.Count;
|
||||
}
|
||||
element.CountLabel.Text = element.Count.ToString();
|
||||
|
||||
if (element.Count == 1)
|
||||
{
|
||||
// There was only one entity in the sub-menu. So we will just remove the sub-menu and point directly to
|
||||
// that entity.
|
||||
element.Entity = entity;
|
||||
element.SubMenu.Dispose();
|
||||
element.SubMenu = null;
|
||||
element.CountLabel.Visible = false;
|
||||
Elements[entity] = element;
|
||||
}
|
||||
|
||||
// update the parent element, so that it's count and entity icon gets updated.
|
||||
var parent = element.ParentMenu?.ParentElement;
|
||||
if (parent is EntityMenuElement e)
|
||||
UpdateElement(e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Look through a sub-menu and return the first entity.
|
||||
/// </summary>
|
||||
private IEntity? GetFirstEntityOrNull(ContextMenuPopup? menu)
|
||||
{
|
||||
if (menu == null)
|
||||
return null;
|
||||
|
||||
foreach (var element in menu.MenuBody.Children)
|
||||
{
|
||||
if (element is not EntityMenuElement entityElement)
|
||||
continue;
|
||||
|
||||
if (entityElement.Entity != null)
|
||||
return entityElement.Entity;
|
||||
|
||||
var entity = GetFirstEntityOrNull(entityElement.SubMenu);
|
||||
if (entity != null)
|
||||
return entity;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void OpenSubMenu(ContextMenuElement element)
|
||||
{
|
||||
base.OpenSubMenu(element);
|
||||
|
||||
// In case the verb menu is currently open, ensure that it is shown ABOVE the entity menu.
|
||||
if (_verbSystem.VerbMenu.Menus.TryPeek(out var menu) && menu.Visible)
|
||||
{
|
||||
menu.ParentElement?.ParentMenu?.SetPositionLast();
|
||||
menu.SetPositionLast();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,8 +77,7 @@ namespace Content.Client.Items.Managers
|
||||
}
|
||||
else if (args.Function == ContentKeyFunctions.OpenContextMenu)
|
||||
{
|
||||
_entitySystemManager.GetEntitySystem<VerbSystem>()
|
||||
.OpenVerbMenu(item, _uiMgr.ScreenToUIPosition(args.PointerLocation));
|
||||
_entitySystemManager.GetEntitySystem<VerbSystem>().VerbMenu.OpenVerbMenu(item);
|
||||
}
|
||||
else if (args.Function == ContentKeyFunctions.ActivateItemInWorld)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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<PanelContainer>().Class(ContextMenuPopup.StyleClassContextMenuPopup)
|
||||
.Prop(PanelContainer.StylePropertyPanel, contextMenuBackground),
|
||||
|
||||
// Context menu buttons
|
||||
Element<ContextMenuElement>().Class(ContextMenuElement.StyleClassContextMenuButton)
|
||||
.Prop(ContainerButton.StylePropertyStyleBox, buttonContext),
|
||||
|
||||
Element<ContextMenuElement>().Class(ContextMenuElement.StyleClassContextMenuButton)
|
||||
.Pseudo(ContainerButton.StylePseudoClassNormal)
|
||||
.Prop(Control.StylePropertyModulateSelf, ButtonColorContext),
|
||||
|
||||
Element<ContextMenuElement>().Class(ContextMenuElement.StyleClassContextMenuButton)
|
||||
.Pseudo(ContainerButton.StylePseudoClassHover)
|
||||
.Prop(Control.StylePropertyModulateSelf, ButtonColorContextHover),
|
||||
|
||||
Element<ContextMenuElement>().Class(ContextMenuElement.StyleClassContextMenuButton)
|
||||
.Pseudo(ContainerButton.StylePseudoClassPressed)
|
||||
.Prop(Control.StylePropertyModulateSelf, ButtonColorContextPressed),
|
||||
|
||||
Element<ContextMenuElement>().Class(ContextMenuElement.StyleClassContextMenuButton)
|
||||
.Pseudo(ContainerButton.StylePseudoClassDisabled)
|
||||
.Prop(Control.StylePropertyModulateSelf, ButtonColorContextDisabled),
|
||||
|
||||
// Context Menu Labels
|
||||
Element<RichTextLabel>().Class(VerbMenuElement.StyleClassVerbInteractionText)
|
||||
.Prop(Label.StylePropertyFont, notoSansBoldItalic12),
|
||||
|
||||
Element<RichTextLabel>().Class(VerbMenuElement.StyleClassVerbActivationText)
|
||||
.Prop(Label.StylePropertyFont, notoSansBold12),
|
||||
|
||||
Element<RichTextLabel>().Class(VerbMenuElement.StyleClassVerbAlternativeText)
|
||||
.Prop(Label.StylePropertyFont, notoSansItalic12),
|
||||
|
||||
Element<RichTextLabel>().Class(VerbMenuElement.StyleClassVerbOtherText)
|
||||
.Prop(Label.StylePropertyFont, notoSans12),
|
||||
|
||||
// Thin buttons (No padding nor vertical margin)
|
||||
Element<EntityContainerButton>().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),
|
||||
|
||||
69
Content.Client/Verbs/UI/VerbMenuElement.cs
Normal file
69
Content.Client/Verbs/UI/VerbMenuElement.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Slight extension of <see cref="ContextMenuElement"/> that uses a SpriteSpecifier for it's icon and provides
|
||||
/// constructors that take verbs or verb categories.
|
||||
/// </summary>
|
||||
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) { }
|
||||
}
|
||||
}
|
||||
200
Content.Client/Verbs/UI/VerbMenuPresenter.cs
Normal file
200
Content.Client/Verbs/UI/VerbMenuPresenter.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// This class handles the displaying of the verb menu.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In addition to the normal <see cref="ContextMenuPresenter"/> 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.
|
||||
/// </remarks>
|
||||
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<VerbType, SortedSet<Verb>> CurrentVerbs = new();
|
||||
|
||||
public VerbMenuPresenter(VerbSystem verbSystem) : base()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_verbSystem = verbSystem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open a verb menu and fill it work verbs applicable to the given target entity.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fill the verb pop-up using the verbs stored in <see cref="CurrentVerbs"/>
|
||||
/// </summary>
|
||||
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<string> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a verb category button to the pop-up
|
||||
/// </summary>
|
||||
public void AddVerbCategory(VerbCategory category, SortedSet<Verb> verbs, VerbType type)
|
||||
{
|
||||
// Get a list of the verbs in this category
|
||||
List<Verb> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add verbs from the server to <see cref="CurrentVerbs"/> and update the verb menu.
|
||||
/// </summary>
|
||||
public void AddServerVerbs(Dictionary<VerbType, List<Verb>>? 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<Verb>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// This pop-up appears when hovering over a verb category in the context menu.
|
||||
/// </summary>
|
||||
public sealed class VerbCategoryPopup : ContextMenuPopup
|
||||
{
|
||||
public VerbCategoryPopup(VerbSystem system, IEnumerable<Verb> 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;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to hide member verb text and just show icons.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If no members have icons, this option is ignored and text is shown anyways. Defaults to using <see cref="VerbCategory.IconsOnly"/>.
|
||||
/// </remarks>
|
||||
private readonly bool _drawOnlyIcons;
|
||||
|
||||
/// <summary>
|
||||
/// The pop-up that appears when hovering over this verb group.
|
||||
/// </summary>
|
||||
private readonly VerbCategoryPopup _popup;
|
||||
|
||||
public VerbCategoryButton(VerbSystem system, VerbCategory category, IEnumerable<Verb> 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<IResourceCache>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open a verb category pop-up after a short delay.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancel the delayed pop-up
|
||||
/// </summary>
|
||||
protected override void MouseExited()
|
||||
{
|
||||
base.MouseExited();
|
||||
|
||||
_openCancel?.Cancel();
|
||||
_openCancel = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
/// <summary>
|
||||
/// When a user right clicks somewhere, how large is the box we use to get entities for the context menu?
|
||||
/// </summary>
|
||||
public const float EntityMenuLookupSize = 1f;
|
||||
|
||||
public EntityUid CurrentTarget;
|
||||
public ContextMenuPopup? CurrentVerbPopup;
|
||||
public ContextMenuPopup? CurrentCategoryPopup;
|
||||
public Dictionary<VerbType, SortedSet<Verb>> CurrentVerbs = new();
|
||||
public EntityMenuPresenter EntityMenu = default!;
|
||||
public VerbMenuPresenter VerbMenu = default!;
|
||||
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to show all entities on the context menu.
|
||||
/// These flags determine what entities the user can see on the context menu.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Verb execution will only be affected if the server also agrees that this player can see the target
|
||||
/// entity.
|
||||
/// </remarks>
|
||||
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<RoundRestartCleanupEvent>(Reset);
|
||||
SubscribeNetworkEvent<VerbsResponseEvent>(HandleVerbResponse);
|
||||
SubscribeNetworkEvent<SetSeeAllContextEvent>(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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public void TryExecuteVerb(Verb verb, EntityUid target, VerbType verbType)
|
||||
public bool TryGetEntityMenuEntities(MapCoordinates targetPos, [NotNullWhen(true)] out List<IEntity>? 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public Dictionary<VerbType, SortedSet<Verb>> 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)
|
||||
/// <summary>
|
||||
/// Execute actions associated with the given verb.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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<Verb> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a list of verbs to a BoxContainer. Iterates over the given verbs list and creates GUI buttons.
|
||||
/// </summary>
|
||||
private void AddVerbSet(ContextMenuPopup popup, VerbType type)
|
||||
{
|
||||
if (!CurrentVerbs.TryGetValue(type, out var verbSet) || verbSet.Count == 0)
|
||||
return;
|
||||
|
||||
HashSet<string> 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Highlight the currently hovered entity.
|
||||
/// </summary>
|
||||
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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<VerbSystem>().ToggleSeeAllContext(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
|
||||
/// <summary>
|
||||
/// List of players that can see all entities on the context menu, ignoring normal visibility rules.
|
||||
/// </summary>
|
||||
public readonly HashSet<IPlayerSession> SeeAllContextPlayers = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
||||
SubscribeNetworkEvent<RequestServerVerbsEvent>(HandleVerbRequest);
|
||||
SubscribeNetworkEvent<TryExecuteVerbEvent>(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<ExecuteVerbEvent>(HandleTryExecuteVerb);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when asked over the network to run a given verb.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<int> ContextMenuGroupingType = CVarDef.Create("context_menu", 0, CVar.CLIENTONLY);
|
||||
public static readonly CVarDef<int> EntityMenuGroupingType = CVarDef.Create("entity_menu", 0, CVar.CLIENTONLY);
|
||||
|
||||
/*
|
||||
* VOTE
|
||||
|
||||
@@ -530,7 +530,7 @@ namespace Content.Shared.Interaction
|
||||
if (verb.Disabled)
|
||||
continue;
|
||||
|
||||
_verbSystem.TryExecuteVerb(verb);
|
||||
_verbSystem.ExecuteVerb(verb);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
@@ -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!;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="buffer">Whether we should slightly extend the entity search area.</param>
|
||||
public bool TryGetContextEntities(IEntity player, MapCoordinates targetPos,
|
||||
[NotNullWhen(true)] out List<IEntity>? 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises a number of events in order to get all verbs of the given type(s)
|
||||
/// </summary>
|
||||
public Dictionary<VerbType, SortedSet<Verb>> GetVerbs(IEntity target, IEntity user, VerbType verbTypes)
|
||||
public virtual Dictionary<VerbType, SortedSet<Verb>> GetLocalVerbs(IEntity target, IEntity user, VerbType verbTypes)
|
||||
{
|
||||
Dictionary<VerbType, SortedSet<Verb>> verbs = new();
|
||||
|
||||
@@ -109,41 +45,24 @@ namespace Content.Shared.Verbs
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute actions associated with the given verb.
|
||||
/// Execute the provided verb.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,37 +28,37 @@ namespace Content.Shared.Verbs
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This delegate probably just points to some function in the system assembling this verb. This delegate
|
||||
/// will be run regardless of whether <see cref="LocalVerbEventArgs"/> or <see cref="NetworkVerbEventArgs"/>
|
||||
/// are defined.
|
||||
/// will be run regardless of whether <see cref="ExecutionEventArgs"/> is defined.
|
||||
/// </remarks>
|
||||
[NonSerialized]
|
||||
public Action? Act;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This event will be raised regardless of whether <see cref="NetworkVerbEventArgs"/> or <see cref="Act"/>
|
||||
/// are defined.
|
||||
/// If not null, this event will be raised regardless of whether <see cref="Act"/> was run. If this event
|
||||
/// exists purely to call a specific system method, then <see cref="Act"/> should probably be used instead (method
|
||||
/// events are a no-go).
|
||||
/// </remarks>
|
||||
[NonSerialized]
|
||||
public object? LocalVerbEventArgs;
|
||||
public object? ExecutionEventArgs;
|
||||
|
||||
/// <summary>
|
||||
/// Where do direct the local event.
|
||||
/// Where do direct the local event. If invalid, the event is not raised directed at any entity.
|
||||
/// </summary>
|
||||
[NonSerialized]
|
||||
public EntityUid LocalEventTarget = EntityUid.Invalid;
|
||||
public EntityUid EventTarget = EntityUid.Invalid;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This event will be raised regardless of whether <see cref="LocalVerbEventArgs"/> or <see cref="Act"/>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
[NonSerialized]
|
||||
public EntityEventArgs? NetworkVerbEventArgs;
|
||||
public bool ClientExclusive;
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
|
||||
/// <summary>
|
||||
@@ -86,18 +87,20 @@ namespace Content.Shared.Verbs
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Disabled verbs are shown in the context menu with a slightly darker background color, and cannot be
|
||||
/// executed. It is recommended that a <see cref="Tooltip"/> message be provided outlining why this verb is
|
||||
/// executed. It is recommended that a <see cref="Message"/> message be provided outlining why this verb is
|
||||
/// disabled.
|
||||
/// </remarks>
|
||||
public bool Disabled;
|
||||
|
||||
/// <summary>
|
||||
/// Optional tooltip to show when hovering over this verb.
|
||||
/// Optional informative message.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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
|
||||
/// <see cref="Disabled"/> 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.
|
||||
/// </remarks>
|
||||
public string? Tooltip;
|
||||
public string? Message;
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
|
||||
/// <summary>
|
||||
/// Raw texture path used to load the <see cref="Icon"/>.
|
||||
/// Raw texture path used to load the <see cref="Icon"/> for displaying on the client.
|
||||
/// </summary>
|
||||
public string? IconTexture;
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ namespace Content.Shared.Verbs
|
||||
public readonly SpriteSpecifier? Icon;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For example, the 'Rotate' category simply shows two icons for rotating left and right.
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event used to toggle visibility of all context menu entities.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public class SetSeeAllContextEvent : EntityEventArgs
|
||||
{
|
||||
public bool CanSeeAllContext = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request primary interaction verbs. This includes both use-in-hand and interacting with external entities.
|
||||
/// </summary>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user