#nullable enable using System; using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; using Microsoft.Xna.Framework; namespace Barotrauma { struct ContextMenuOption { public string Label; public Action OnSelected; public ContextMenuOption[]? SubOptions; public bool IsEnabled; public string Tooltip; // Creates a regular context menu public ContextMenuOption(string label, bool isEnabled, Action onSelected) { Label = TextManager.Get(label, returnNull: true) ?? label; OnSelected = onSelected; IsEnabled = isEnabled; SubOptions = null; Tooltip = string.Empty; } // Creates a option with a sub context menu public ContextMenuOption(string label, bool isEnabled, params ContextMenuOption[] options): this(label, isEnabled, () => { }) { SubOptions = options; } } internal class GUIContextMenu : GUIComponent { public static GUIContextMenu? CurrentContextMenu; private readonly Dictionary Options = new Dictionary(); private GUIContextMenu? SubMenu; public readonly GUITextBlock? HeaderLabel; public GUITextBlock? ParentOption; /// /// Creates a context menu. This constructor does not make the context menu active. /// Use to make right click context menus. /// /// Position at which to create the context menu /// Header text /// Background style /// list of context menu options public GUIContextMenu(Vector2? position, string header, string style, params ContextMenuOption[] options) : base(style, new RectTransform(Point.Zero, GUI.Canvas)) { Vector2 pos = position ?? PlayerInput.MousePosition; ScalableFont headerFont = GUI.SubHeadingFont; ScalableFont font = GUI.SmallFont; // font the context menu options use Vector4 padding = new Vector4(4), headerPadding = new Vector4(8); int horizontalPadding = (int) (padding.X + padding.Z), verticalPadding = (int) (padding.Y + padding.W); bool hasHeader = !string.IsNullOrWhiteSpace(header); //---------------------------------------------------------------------------------- // Estimate the size of the context menu //---------------------------------------------------------------------------------- Dictionary optionsAndSizes = new Dictionary(); // estimate how big the context menu needs to be Point estimatedSize = new Point(horizontalPadding, verticalPadding); if (hasHeader) { InflateSize(ref estimatedSize, header, headerFont); } foreach (ContextMenuOption option in options) { Vector2 optionSize = InflateSize(ref estimatedSize, option.Label, font); optionsAndSizes.Add(option, optionSize); } // it's better to overestimate the size since it's going to be cropped anyways estimatedSize = estimatedSize.Multiply(1.2f); RectTransform.NonScaledSize = estimatedSize; RectTransform.AbsoluteOffset = pos.ToPoint(); //---------------------------------------------------------------------------------- // Construct the GUI elements //---------------------------------------------------------------------------------- GUILayoutGroup background = new GUILayoutGroup(new RectTransform(Vector2.One, RectTransform, Anchor.Center)); if (hasHeader) { HeaderLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.2f), background.RectTransform), header, font: headerFont) { Padding = headerPadding }; } GUIListBox optionList = new GUIListBox(new RectTransform(new Vector2(1f, hasHeader ? 0.8f : 1f), background.RectTransform), style: null) { AutoHideScrollBar = false, ScrollBarVisible = false, Padding = hasHeader ? new Vector4(4, 0, 4, 4) : padding }; foreach (var (option, size) in optionsAndSizes) { GUITextBlock optionElement = new GUITextBlock(new RectTransform(size.ToPoint(), optionList.Content.RectTransform), option.Label, font: font) { UserData = option, Enabled = option.IsEnabled }; Options.Add(option, optionElement); if (!string.IsNullOrWhiteSpace(option.Tooltip) && optionElement.Enabled) { optionElement.ToolTip = option.Tooltip; } if (!option.IsEnabled) { optionElement.TextColor *= 0.5f; } } //---------------------------------------------------------------------------------- // Positioning and cropping the context menu //---------------------------------------------------------------------------------- List children = optionList.Content.Children.ToList(); // Resize all children to the size of their text foreach (GUITextBlock block in children.Where(c => c is GUITextBlock).Cast()) { block.RectTransform.NonScaledSize = new Point((int) (block.TextSize.X + (block.Padding.X + block.Padding.Z)), (int) (18 * GUI.Scale)); } int largestWidth = children.Max(c => c.Rect.Width + horizontalPadding); // if the header is bigger than any of the options then overwrite if (HeaderLabel != null) { RectTransform headerTransform = HeaderLabel.RectTransform; headerTransform.MinSize = new Point((int) (HeaderLabel.TextSize.X + (headerPadding.X + headerPadding.Z)), headerTransform.NonScaledSize.Y); if (largestWidth < headerTransform.MinSize.X) { largestWidth = headerTransform.MinSize.X; } } // resize all children to the size of the longest element foreach (GUIComponent c in children) { c.RectTransform.MinSize = new Point(largestWidth, c.Rect.Height); } // the cropped size of the option list Point newSize = new Point(largestWidth, children.Sum(c => c.Rect.Height) + verticalPadding); // resize the menu itself taking into account the option menus relative Y size RectTransform.NonScaledSize = new Point(newSize.X, (int) (newSize.Y / optionList.RectTransform.RelativeSize.Y)); optionList.RectTransform.NonScaledSize = newSize; // move the context menu if it would go outside of screen if (RectTransform.Rect.Bottom > GameMain.GraphicsHeight) { Rectangle rect = RectTransform.Rect; RectTransform.AbsoluteOffset = new Point(rect.X, rect.Y - rect.Height); } if (RectTransform.Rect.Right > GameMain.GraphicsWidth) { Rectangle rect = RectTransform.Rect; RectTransform.AbsoluteOffset = new Point(rect.X - rect.Width, rect.Y); } background.Recalculate(); optionList.OnSelected = OnSelected; } public static GUIContextMenu CreateContextMenu(params ContextMenuOption[] options) => CreateContextMenu(PlayerInput.MousePosition, string.Empty, null, options); public static GUIContextMenu CreateContextMenu(Vector2? pos, string header, Color? headerColor, params ContextMenuOption[] options) { GUIContextMenu menu = new GUIContextMenu(pos,header, "GUIToolTip", options); if (headerColor != null) { menu.HeaderLabel?.OverrideTextColor(headerColor.Value); } CurrentContextMenu = menu; return menu; } private bool OnSelected(GUIComponent _, object data) { if (data is ContextMenuOption option && option.IsEnabled) { CurrentContextMenu = null; option.OnSelected(); return true; } return false; } /// /// Inflates a point by the size of the text /// /// Pint to resize /// String whose size to inflate by /// What font to use /// The size of the text private Vector2 InflateSize(ref Point size, string label, ScalableFont font) { Vector2 textSize = font.MeasureString(label); size.X = Math.Max((int) Math.Ceiling(textSize.X), size.X); size.Y += (int) Math.Ceiling(textSize.Y); return textSize; } protected override void Update(float deltaTime) { base.Update(deltaTime); // keep the parent highlighted if (ParentOption != null) { ParentOption.State = ComponentState.Hover; } if (SubMenu != null && !SubMenu.IsMouseOver()) { SubMenu = null; return; } foreach (var (option, textBlock) in Options) { // Create a new sub context menu if hovering over an option with sub options if (GUI.MouseOn != textBlock) { continue; } if (option.IsEnabled && option.SubOptions is { } subOptions && subOptions.Any()) { Vector2 subMenuPos = new Vector2(textBlock.MouseRect.Right + 4, textBlock.MouseRect.Y); SubMenu = new GUIContextMenu(subMenuPos, "", "GUIToolTip", subOptions) { ParentOption = textBlock }; } } } /// /// Checks if the mouse cursor is over this context menu or any of its sub menus /// /// private bool IsMouseOver() { Rectangle expandedRect = Rect; expandedRect.Inflate(20, 20); bool isMouseOn = expandedRect.Contains(PlayerInput.MousePosition); if (ParentOption != null) { isMouseOn |= GUI.MouseOn == ParentOption; } // Recursively check sub context menus if (!isMouseOn && SubMenu != null) { isMouseOn = SubMenu.IsMouseOver(); } return isMouseOn; } public override void AddToGUIUpdateList(bool ignoreChildren = false, int order = 0) { base.AddToGUIUpdateList(ignoreChildren, order); SubMenu?.AddToGUIUpdateList(); } public static void AddActiveToGUIUpdateList() { if (CurrentContextMenu != null && !CurrentContextMenu.IsMouseOver()) { CurrentContextMenu = null; } CurrentContextMenu?.AddToGUIUpdateList(); } } }