Client LuaCsForBarotrauma
GUIContextMenu.cs
1 #nullable enable
2 using System;
3 using System.Collections.Generic;
4 using System.Linq;
6 using Microsoft.Xna.Framework;
7 
8 namespace Barotrauma
9 {
11  {
13  public Action OnSelected;
15  public bool IsEnabled;
17 
18 
19  public ContextMenuOption(string label, bool isEnabled, Action onSelected)
20  : this(TextManager.Get(label).Fallback(label), isEnabled, onSelected) { }
21 
22  public ContextMenuOption(Identifier labelTag, bool isEnabled, Action onSelected)
23  : this(TextManager.Get(labelTag), isEnabled, onSelected) { }
24 
25  // Creates a regular context menu
26  public ContextMenuOption(LocalizedString label, bool isEnabled, Action onSelected)
27  {
28  Label = label;
29  OnSelected = onSelected;
30  IsEnabled = isEnabled;
31  SubOptions = null;
32  Tooltip = string.Empty;
33  }
34 
35  // Creates a option with a sub context menu
36  public ContextMenuOption(string label, bool isEnabled, params ContextMenuOption[] options): this(label, isEnabled, () => { })
37  {
38  SubOptions = options;
39  }
40  }
41 
42  internal class GUIContextMenu : GUIComponent
43  {
44  public static GUIContextMenu? CurrentContextMenu;
45 
46  private readonly Dictionary<ContextMenuOption, GUITextBlock> Options = new Dictionary<ContextMenuOption, GUITextBlock>();
47  private GUIContextMenu? SubMenu;
48  public readonly GUITextBlock? HeaderLabel;
49  public GUITextBlock? ParentOption;
50 
59  public GUIContextMenu(Vector2? position, LocalizedString header, string style, params ContextMenuOption[] options) : base(style, new RectTransform(Point.Zero, GUI.Canvas))
60  {
61  Vector2 pos = position ?? PlayerInput.MousePosition;
62  GUIFont headerFont = GUIStyle.SubHeadingFont;
63  GUIFont font = GUIStyle.SmallFont; // font the context menu options use
64  Vector4 padding = new Vector4(4), headerPadding = new Vector4(8);
65  int horizontalPadding = (int)(padding.X + padding.Z), verticalPadding = (int)(padding.Y + padding.W);
66  bool hasHeader = !header.IsNullOrWhiteSpace();
67 
68  //----------------------------------------------------------------------------------
69  // Estimate the size of the context menu
70  //----------------------------------------------------------------------------------
71 
72  Dictionary<ContextMenuOption, Vector2> optionsAndSizes = new Dictionary<ContextMenuOption, Vector2>();
73 
74  // estimate how big the context menu needs to be
75  Point estimatedSize = new Point(horizontalPadding, verticalPadding);
76 
77  if (hasHeader)
78  {
79  InflateSize(ref estimatedSize, header, headerFont!);
80  }
81 
82  foreach (ContextMenuOption option in options)
83  {
84  Vector2 optionSize = InflateSize(ref estimatedSize, option.Label, font!);
85  optionsAndSizes.Add(option, optionSize);
86  }
87 
88  // it's better to overestimate the size since it's going to be cropped anyways
89  estimatedSize = estimatedSize.Multiply(1.2f);
90 
91  RectTransform.NonScaledSize = estimatedSize;
92  RectTransform.AbsoluteOffset = pos.ToPoint();
93 
94  //----------------------------------------------------------------------------------
95  // Construct the GUI elements
96  //----------------------------------------------------------------------------------
97 
98  GUILayoutGroup background = new GUILayoutGroup(new RectTransform(Vector2.One, RectTransform, Anchor.Center))
99  {
100  Stretch = true
101  };
102 
103  Point listSize = estimatedSize;
104  if (hasHeader)
105  {
106  Point sz = Point.Zero;
107  InflateSize(ref sz, header, headerFont!);
108  listSize.Y -= sz.Y;
109  HeaderLabel = new GUITextBlock(new RectTransform(sz, background.RectTransform), header, font: headerFont) { Padding = headerPadding };
110  }
111 
112  GUIListBox optionList = new GUIListBox(new RectTransform(listSize, background.RectTransform), style: null)
113  {
114  AutoHideScrollBar = false,
115  ScrollBarVisible = false,
116  Padding = hasHeader ? new Vector4(4, 0, 4, 4) : padding,
117  PlaySoundOnSelect = true
118  };
119 
120  foreach (var (option, size) in optionsAndSizes)
121  {
122  GUITextBlock optionElement = new GUITextBlock(new RectTransform(size.ToPoint(), optionList.Content.RectTransform), option.Label, font: font)
123  {
124  UserData = option,
125  Enabled = option.IsEnabled
126  };
127  Options.Add(option, optionElement);
128 
129  if (!option.Tooltip.IsNullOrWhiteSpace() && optionElement.Enabled)
130  {
131  optionElement.ToolTip = option.Tooltip;
132  }
133 
134  //option doesn't do anything, make it a label
135  if (option.OnSelected == null)
136  {
137  optionElement.TextAlignment = Alignment.BottomLeft;
138  optionElement.TextColor = optionElement.DisabledTextColor = GUIStyle.Green;
139  }
140  else if (!option.IsEnabled)
141  {
142  optionElement.TextColor *= 0.5f;
143  }
144  }
145 
146  //----------------------------------------------------------------------------------
147  // Positioning and cropping the context menu
148  //----------------------------------------------------------------------------------
149 
150  List<GUIComponent> children = optionList.Content.Children.ToList();
151 
152  // Resize all children to the size of their text
153  foreach (GUITextBlock block in children.Where(c => c is GUITextBlock).Cast<GUITextBlock>())
154  {
155  bool isLabel = block.UserData is ContextMenuOption option && option.OnSelected == null;
156  block.RectTransform.NonScaledSize = new Point(
157  (int)(block.TextSize.X + (block.Padding.X + block.Padding.Z)),
158  (int)Math.Max(block.TextSize.Y * 1.2f, 18 * GUI.Scale));
159  }
160 
161  int largestWidth = children.Max(c => c.Rect.Width + horizontalPadding);
162 
163  // if the header is bigger than any of the options then overwrite
164  if (HeaderLabel != null)
165  {
166  RectTransform headerTransform = HeaderLabel.RectTransform;
167  headerTransform.MinSize = new Point((int)(HeaderLabel.TextSize.X + (headerPadding.X + headerPadding.Z)), headerTransform.NonScaledSize.Y);
168  if (largestWidth < headerTransform.MinSize.X)
169  {
170  largestWidth = headerTransform.MinSize.X;
171  }
172  }
173 
174  // resize all children to the size of the longest element
175  foreach (GUIComponent c in children)
176  {
177  c.RectTransform.MinSize = new Point(largestWidth, c.Rect.Height);
178  }
179 
180  // the cropped size of the option list
181  Point newSize = new Point(largestWidth, children.Sum(c => c.Rect.Height) + verticalPadding);
182  // resize the menu itself taking into account the option menus relative Y size
183  RectTransform.NonScaledSize = new Point(newSize.X, (int)(newSize.Y / optionList.RectTransform.RelativeSize.Y));
184  optionList.RectTransform.NonScaledSize = newSize;
185 
186  // move the context menu if it would go outside of screen
187  if (RectTransform.Rect.Bottom > GameMain.GraphicsHeight)
188  {
190  RectTransform.AbsoluteOffset = new Point(rect.X, rect.Y - rect.Height);
191  }
192 
193  if (RectTransform.Rect.Right > GameMain.GraphicsWidth)
194  {
196  RectTransform.AbsoluteOffset = new Point(rect.X - rect.Width, rect.Y);
197  }
198 
199  background.Recalculate();
200 
201  optionList.OnSelected = OnSelected;
202  }
203 
204  public static GUIContextMenu CreateContextMenu(params ContextMenuOption[] options) => CreateContextMenu(PlayerInput.MousePosition, string.Empty, null, options);
205 
206  public static GUIContextMenu CreateContextMenu(Vector2? pos, LocalizedString header, Color? headerColor, params ContextMenuOption[] options)
207  {
208  GUIContextMenu menu = new GUIContextMenu(pos,header, "GUIToolTip", options);
209  if (headerColor != null)
210  {
211  menu.HeaderLabel?.OverrideTextColor(headerColor.Value);
212  }
213  CurrentContextMenu = menu;
214  return menu;
215  }
216 
217  private bool OnSelected(GUIComponent _, object data)
218  {
219  if (data is ContextMenuOption option && option.IsEnabled)
220  {
221  CurrentContextMenu = null;
222  option.OnSelected();
223  return true;
224  }
225 
226  return false;
227  }
228 
236  private Vector2 InflateSize(ref Point size, LocalizedString label, ScalableFont font)
237  {
238  Vector2 textSize = font.MeasureString(label);
239  size.X = Math.Max((int)Math.Ceiling(textSize.X), size.X);
240  size.Y += (int)Math.Ceiling(textSize.Y);
241  return textSize;
242  }
243 
244  protected override void Update(float deltaTime)
245  {
246  base.Update(deltaTime);
247 
248  // keep the parent highlighted
249  if (ParentOption != null)
250  {
251  ParentOption.State = ComponentState.Hover;
252  }
253 
254  if (SubMenu != null && !SubMenu.IsMouseOver())
255  {
256  SubMenu = null;
257  return;
258  }
259 
260  foreach (var (option, textBlock) in Options)
261  {
262  // Create a new sub context menu if hovering over an option with sub options
263  if (GUI.MouseOn != textBlock) { continue; }
264  if (option.IsEnabled && option.SubOptions is { } subOptions && subOptions.Any())
265  {
266  Vector2 subMenuPos = new Vector2(textBlock.MouseRect.Right + 4, textBlock.MouseRect.Y);
267  SubMenu = new GUIContextMenu(subMenuPos, "", "GUIToolTip", subOptions)
268  {
269  ParentOption = textBlock
270  };
271  }
272  }
273  }
274 
279  private bool IsMouseOver()
280  {
281  Rectangle expandedRect = Rect;
282  expandedRect.Inflate(20, 20);
283 
284  bool isMouseOn = expandedRect.Contains(PlayerInput.MousePosition);
285 
286  if (ParentOption != null)
287  {
288  isMouseOn |= GUI.MouseOn == ParentOption;
289  }
290 
291  // Recursively check sub context menus
292  if (!isMouseOn && SubMenu != null)
293  {
294  isMouseOn = SubMenu.IsMouseOver();
295  }
296 
297  return isMouseOn;
298  }
299 
300  public override void AddToGUIUpdateList(bool ignoreChildren = false, int order = 0)
301  {
302  base.AddToGUIUpdateList(ignoreChildren, order);
303  SubMenu?.AddToGUIUpdateList(order: 2);
304  }
305 
306  public static void AddActiveToGUIUpdateList()
307  {
308  if (CurrentContextMenu != null && !CurrentContextMenu.IsMouseOver())
309  {
310  CurrentContextMenu = null;
311  }
312 
313  CurrentContextMenu?.AddToGUIUpdateList(order: 2);
314  }
315  }
316 }
virtual bool PlaySoundOnSelect
virtual Rectangle Rect
RectTransform RectTransform
Defines a point in the event that GoTo actions can jump to.
Definition: Label.cs:7
Point AbsoluteOffset
Absolute in pixels but relative to the anchor point. Calculated away from the anchor point,...
RectTransform(Vector2 relativeSize, RectTransform parent, Anchor anchor=Anchor.TopLeft, Pivot? pivot=null, Point? minSize=null, Point? maxSize=null, ScaleBasis scaleBasis=ScaleBasis.Normal)
Point NonScaledSize
Size before scale multiplications.
ContextMenuOption(Identifier labelTag, bool isEnabled, Action onSelected)
ContextMenuOption(LocalizedString label, bool isEnabled, Action onSelected)
ContextMenuOption?[] SubOptions
ContextMenuOption(string label, bool isEnabled, params ContextMenuOption[] options)
ContextMenuOption(string label, bool isEnabled, Action onSelected)