Files
tbd-station-14/Content.Client/ContextMenu/UI/ContextMenuPresenter.cs
Leon Friedrich 49296e33a0 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
2021-10-27 22:21:19 -07:00

180 lines
6.3 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading;
using Robust.Client.UserInterface;
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
{
public static readonly TimeSpan HoverDelay = TimeSpan.FromSeconds(0.2);
public ContextMenuPopup RootMenu;
public Stack<ContextMenuPopup> Menus { get; } = new();
/// <summary>
/// Used to cancel the timer that opens menus.
/// </summary>
public CancellationTokenSource? CancelOpen;
/// <summary>
/// Used to cancel the timer that closes menus.
/// </summary>
public CancellationTokenSource? CancelClose;
public ContextMenuPresenter()
{
RootMenu = new(this, null);
RootMenu.OnPopupHide += RootMenu.MenuBody.DisposeAllChildren;
Menus.Push(RootMenu);
}
/// <summary>
/// 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>
/// 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>
public void CloseSubMenus(ContextMenuPopup? menu)
{
if (menu == null || !menu.Visible)
return;
while (Menus.TryPeek(out var subMenu) && subMenu != menu)
{
Menus.Pop().Close();
}
}
/// <summary>
/// Start a timer to open this element's sub-menu.
/// </summary>
public virtual void OnMouseEntered(ContextMenuElement element)
{
var topMenu = Menus.Peek();
if (element.ParentMenu == topMenu || element.SubMenu == topMenu)
CancelClose?.Cancel();
if (element.SubMenu == topMenu)
return;
// open the sub-menu after a short delay.
CancelOpen?.Cancel();
CancelOpen = new();
Timer.Spawn(HoverDelay, () => OpenSubMenu(element), CancelOpen.Token);
}
/// <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;
CancelClose?.Cancel();
CancelClose = new();
Timer.Spawn(HoverDelay, () => CloseSubMenus(element.ParentMenu), CancelClose.Token);
}
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)
{
// If This is already the top most menu, do nothing.
if (element.SubMenu == Menus.Peek())
return;
// Was the parent menu closed or disposed before an open timer completed?
if (element.Disposed || element.ParentMenu == null || !element.ParentMenu.Visible)
return;
// Close any currently open sub-menus up to this element's parent menu.
CloseSubMenus(element.ParentMenu);
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();
}
}
}