A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System;
5using System.Collections.Generic;
6using System.Linq;
7using osu.Framework.Extensions.EnumExtensions;
8using osu.Framework.Extensions.IEnumerableExtensions;
9using osuTK.Graphics;
10using osu.Framework.Graphics.Containers;
11using osu.Framework.Graphics.Shapes;
12using osu.Framework.Graphics.Sprites;
13using osu.Framework.Input.Events;
14using osu.Framework.Layout;
15using osu.Framework.Utils;
16using osu.Framework.Threading;
17using osuTK;
18using osuTK.Input;
19
20namespace osu.Framework.Graphics.UserInterface
21{
22 public abstract class Menu : CompositeDrawable, IStateful<MenuState>
23 {
24 /// <summary>
25 /// Invoked when this <see cref="Menu"/>'s <see cref="State"/> changes.
26 /// </summary>
27 public event Action<MenuState> StateChanged;
28
29 /// <summary>
30 /// Gets or sets the delay before opening sub-<see cref="Menu"/>s when menu items are hovered.
31 /// </summary>
32 protected double HoverOpenDelay = 100;
33
34 /// <summary>
35 /// Whether this menu is always displayed in an open state (ie. a menu bar).
36 /// Clicks are required to activate <see cref="DrawableMenuItem"/>.
37 /// </summary>
38 protected readonly bool TopLevelMenu;
39
40 /// <summary>
41 /// The <see cref="Container{T}"/> that contains the content of this <see cref="Menu"/>.
42 /// </summary>
43 protected readonly ScrollContainer<Drawable> ContentContainer;
44
45 /// <summary>
46 /// The <see cref="Container{T}"/> that contains the items of this <see cref="Menu"/>.
47 /// </summary>
48 protected FillFlowContainer<DrawableMenuItem> ItemsContainer;
49
50 /// <summary>
51 /// The container that provides the masking effects for this <see cref="Menu"/>.
52 /// </summary>
53 protected readonly Container MaskingContainer;
54
55 /// <summary>
56 /// Gets the item representations contained by this <see cref="Menu"/>.
57 /// </summary>
58 protected internal IReadOnlyList<DrawableMenuItem> Children => ItemsContainer.Children;
59
60 protected readonly Direction Direction;
61
62 private Menu parentMenu;
63 private Menu submenu;
64
65 private readonly Box background;
66
67 private readonly LayoutValue sizeCache = new LayoutValue(Invalidation.RequiredParentSizeToFit, InvalidationSource.Child);
68
69 private readonly Container<Menu> submenuContainer;
70
71 /// <summary>
72 /// Constructs a menu.
73 /// </summary>
74 /// <param name="direction">The direction of layout for this menu.</param>
75 /// <param name="topLevelMenu">Whether the resultant menu is always displayed in an open state (ie. a menu bar).</param>
76 protected Menu(Direction direction, bool topLevelMenu = false)
77 {
78 Direction = direction;
79 TopLevelMenu = topLevelMenu;
80
81 if (topLevelMenu)
82 state = MenuState.Open;
83
84 InternalChildren = new Drawable[]
85 {
86 MaskingContainer = new Container
87 {
88 Name = "Our contents",
89 RelativeSizeAxes = Axes.Both,
90 Masking = true,
91 Children = new Drawable[]
92 {
93 background = new Box
94 {
95 RelativeSizeAxes = Axes.Both,
96 Colour = Color4.Black
97 },
98 ContentContainer = CreateScrollContainer(direction).With(d =>
99 {
100 d.RelativeSizeAxes = Axes.Both;
101 d.Masking = false;
102 d.Child = ItemsContainer = new FillFlowContainer<DrawableMenuItem> { Direction = direction == Direction.Horizontal ? FillDirection.Horizontal : FillDirection.Vertical };
103 })
104 }
105 },
106 submenuContainer = new Container<Menu>
107 {
108 Name = "Sub menu container",
109 AutoSizeAxes = Axes.Both
110 }
111 };
112
113 switch (direction)
114 {
115 case Direction.Horizontal:
116 ItemsContainer.AutoSizeAxes = Axes.X;
117 break;
118
119 case Direction.Vertical:
120 ItemsContainer.AutoSizeAxes = Axes.Y;
121 break;
122 }
123
124 // The menu will provide a valid size for the items container based on our own size
125 ItemsContainer.RelativeSizeAxes = Axes.Both & ~ItemsContainer.AutoSizeAxes;
126
127 AddLayout(sizeCache);
128 }
129
130 protected override void LoadComplete()
131 {
132 base.LoadComplete();
133 updateState();
134 }
135
136 /// <summary>
137 /// Gets or sets the <see cref="MenuItem"/>s contained within this <see cref="Menu"/>.
138 /// </summary>
139 public IReadOnlyList<MenuItem> Items
140 {
141 get => ItemsContainer.Select(r => r.Item).ToList();
142 set
143 {
144 Clear();
145 value?.ForEach(Add);
146 }
147 }
148
149 /// <summary>
150 /// Gets or sets the background colour of this <see cref="Menu"/>.
151 /// </summary>
152 public Color4 BackgroundColour
153 {
154 get => background.Colour;
155 set => background.Colour = value;
156 }
157
158 /// <summary>
159 /// Gets or sets whether the scroll bar of this <see cref="Menu"/> should be visible.
160 /// </summary>
161 public bool ScrollbarVisible
162 {
163 get => ContentContainer.ScrollbarVisible;
164 set => ContentContainer.ScrollbarVisible = value;
165 }
166
167 private float maxWidth = float.MaxValue;
168
169 /// <summary>
170 /// Gets or sets the maximum allowable width by this <see cref="Menu"/>.
171 /// </summary>
172 public float MaxWidth
173 {
174 get => maxWidth;
175 set
176 {
177 if (Precision.AlmostEquals(maxWidth, value))
178 return;
179
180 maxWidth = value;
181
182 sizeCache.Invalidate();
183 }
184 }
185
186 private float maxHeight = float.PositiveInfinity;
187
188 /// <summary>
189 /// Gets or sets the maximum allowable height by this <see cref="Menu"/>.
190 /// </summary>
191 public float MaxHeight
192 {
193 get => maxHeight;
194 set
195 {
196 if (Precision.AlmostEquals(maxHeight, value))
197 return;
198
199 maxHeight = value;
200
201 sizeCache.Invalidate();
202 }
203 }
204
205 private MenuState state = MenuState.Closed;
206
207 /// <summary>
208 /// Gets or sets the current state of this <see cref="Menu"/>.
209 /// </summary>
210 public virtual MenuState State
211 {
212 get => state;
213 set
214 {
215 if (TopLevelMenu)
216 {
217 submenu?.Close();
218 return;
219 }
220
221 if (state == value)
222 return;
223
224 state = value;
225
226 updateState();
227 StateChanged?.Invoke(State);
228 }
229 }
230
231 private void updateState()
232 {
233 if (!IsLoaded)
234 return;
235
236 resetState();
237
238 switch (State)
239 {
240 case MenuState.Closed:
241 AnimateClose();
242
243 if (HasFocus)
244 GetContainingInputManager()?.ChangeFocus(parentMenu);
245 break;
246
247 case MenuState.Open:
248 AnimateOpen();
249
250 // We may not be present at this point, so must run on the next frame.
251 if (!TopLevelMenu)
252 {
253 Schedule(delegate
254 {
255 if (State == MenuState.Open) GetContainingInputManager().ChangeFocus(this);
256 });
257 }
258
259 break;
260 }
261 }
262
263 private void resetState()
264 {
265 if (!IsLoaded)
266 return;
267
268 submenu?.Close();
269 sizeCache.Invalidate();
270 }
271
272 /// <summary>
273 /// Adds a <see cref="MenuItem"/> to this <see cref="Menu"/>.
274 /// </summary>
275 /// <param name="item">The <see cref="MenuItem"/> to add.</param>
276 public virtual void Add(MenuItem item)
277 {
278 var drawableItem = CreateDrawableMenuItem(item);
279 drawableItem.Clicked = menuItemClicked;
280 drawableItem.Hovered = menuItemHovered;
281 drawableItem.StateChanged += s => itemStateChanged(drawableItem, s);
282
283 drawableItem.SetFlowDirection(Direction);
284
285 ItemsContainer.Add(drawableItem);
286 sizeCache.Invalidate();
287 }
288
289 private void itemStateChanged(DrawableMenuItem item, MenuItemState state)
290 {
291 if (state != MenuItemState.Selected) return;
292
293 if (item != selectedItem && selectedItem != null)
294 selectedItem.State = MenuItemState.NotSelected;
295 selectedItem = item;
296 }
297
298 /// <summary>
299 /// Removes a <see cref="MenuItem"/> from this <see cref="Menu"/>.
300 /// </summary>
301 /// <param name="item">The <see cref="MenuItem"/> to remove.</param>
302 /// <returns>Whether <paramref name="item"/> was successfully removed.</returns>
303 public bool Remove(MenuItem item)
304 {
305 bool result = ItemsContainer.RemoveAll(d => d.Item == item) > 0;
306 sizeCache.Invalidate();
307
308 return result;
309 }
310
311 /// <summary>
312 /// Clears all <see cref="MenuItem"/>s in this <see cref="Menu"/>.
313 /// </summary>
314 public void Clear()
315 {
316 ItemsContainer.Clear();
317 resetState();
318 }
319
320 /// <summary>
321 /// Opens this <see cref="Menu"/>.
322 /// </summary>
323 public void Open() => State = MenuState.Open;
324
325 /// <summary>
326 /// Closes this <see cref="Menu"/>.
327 /// </summary>
328 public void Close() => State = MenuState.Closed;
329
330 /// <summary>
331 /// Toggles the state of this <see cref="Menu"/>.
332 /// </summary>
333 public void Toggle() => State = State == MenuState.Closed ? MenuState.Open : MenuState.Closed;
334
335 /// <summary>
336 /// Animates the opening of this <see cref="Menu"/>.
337 /// </summary>
338 protected virtual void AnimateOpen() => Show();
339
340 /// <summary>
341 /// Animates the closing of this <see cref="Menu"/>.
342 /// </summary>
343 protected virtual void AnimateClose() => Hide();
344
345 protected override void UpdateAfterChildren()
346 {
347 base.UpdateAfterChildren();
348
349 if (!sizeCache.IsValid)
350 {
351 // Our children will be relatively-sized on the axis separate to the menu direction, so we need to compute
352 // that size ourselves, based on the content size of our children, to give them a valid relative size
353
354 float width = 0;
355 float height = 0;
356
357 foreach (var item in Children)
358 {
359 width = Math.Max(width, item.ContentDrawWidth);
360 height = Math.Max(height, item.ContentDrawHeight);
361 }
362
363 // When scrolling in one direction, ItemsContainer is auto-sized in that direction and relative-sized in the other
364 // In the case of the auto-sized direction, we want to use its size. In the case of the relative-sized direction, we want
365 // to use the (above) computed size.
366 width = Direction == Direction.Horizontal ? ItemsContainer.Width : width;
367 height = Direction == Direction.Vertical ? ItemsContainer.Height : height;
368
369 width = Math.Min(MaxWidth, width);
370 height = Math.Min(MaxHeight, height);
371
372 // Regardless of the above result, if we are relative-sizing, just use the stored width/height
373 width = RelativeSizeAxes.HasFlagFast(Axes.X) ? Width : width;
374 height = RelativeSizeAxes.HasFlagFast(Axes.Y) ? Height : height;
375
376 if (State == MenuState.Closed && Direction == Direction.Horizontal)
377 width = 0;
378 if (State == MenuState.Closed && Direction == Direction.Vertical)
379 height = 0;
380
381 UpdateSize(new Vector2(width, height));
382
383 sizeCache.Validate();
384 }
385 }
386
387 /// <summary>
388 /// Resizes this <see cref="Menu"/>.
389 /// </summary>
390 /// <param name="newSize">The new size.</param>
391 protected virtual void UpdateSize(Vector2 newSize) => Size = newSize;
392
393 #region Hover/Focus logic
394
395 private void menuItemClicked(DrawableMenuItem item)
396 {
397 // We only want to close the sub-menu if we're not a sub menu - if we are a sub menu
398 // then clicks should instead cause the sub menus to instantly show up
399 if (TopLevelMenu && submenu?.State == MenuState.Open)
400 {
401 submenu.Close();
402 return;
403 }
404
405 // Check if there is a sub menu to display
406 if (item.Item.Items?.Count == 0)
407 {
408 // This item must have attempted to invoke an action - close all menus if item allows
409 if (item.CloseMenuOnClick)
410 closeAll();
411
412 return;
413 }
414
415 openDelegate?.Cancel();
416
417 openSubmenuFor(item);
418 }
419
420 private DrawableMenuItem selectedItem;
421
422 /// <summary>
423 /// The item which triggered opening us as a submenu.
424 /// </summary>
425 private MenuItem triggeringItem;
426
427 private void openSubmenuFor(DrawableMenuItem item)
428 {
429 item.State = MenuItemState.Selected;
430
431 if (submenu == null)
432 {
433 submenuContainer.Add(submenu = CreateSubMenu());
434 submenu.parentMenu = this;
435 submenu.StateChanged += submenuStateChanged;
436 }
437
438 submenu.triggeringItem = item.Item;
439
440 submenu.Items = item.Item.Items;
441 submenu.Position = item.ToSpaceOfOtherDrawable(new Vector2(
442 Direction == Direction.Vertical ? item.DrawWidth : 0,
443 Direction == Direction.Horizontal ? item.DrawHeight : 0), this);
444
445 if (item.Item.Items.Count > 0)
446 {
447 if (submenu.State == MenuState.Open)
448 Schedule(delegate { GetContainingInputManager().ChangeFocus(submenu); });
449 else
450 submenu.Open();
451 }
452 else
453 submenu.Close();
454 }
455
456 private void submenuStateChanged(MenuState state)
457 {
458 switch (state)
459 {
460 case MenuState.Closed:
461 selectedItem.State = MenuItemState.NotSelected;
462 break;
463
464 case MenuState.Open:
465 selectedItem.State = MenuItemState.Selected;
466 break;
467 }
468 }
469
470 private ScheduledDelegate openDelegate;
471
472 private void menuItemHovered(DrawableMenuItem item)
473 {
474 // If we're not a sub-menu, then hover shouldn't display a sub-menu unless an item is clicked
475 if (TopLevelMenu && submenu?.State != MenuState.Open)
476 return;
477
478 openDelegate?.Cancel();
479
480 if (TopLevelMenu || HoverOpenDelay == 0)
481 openSubmenuFor(item);
482 else
483 {
484 openDelegate = Scheduler.AddDelayed(() =>
485 {
486 if (item.IsHovered)
487 openSubmenuFor(item);
488 }, HoverOpenDelay);
489 }
490 }
491
492 public override bool HandleNonPositionalInput => State == MenuState.Open;
493
494 protected override bool OnKeyDown(KeyDownEvent e)
495 {
496 if (e.Key == Key.Escape && !TopLevelMenu)
497 {
498 Close();
499 return true;
500 }
501
502 return base.OnKeyDown(e);
503 }
504
505 protected override bool OnClick(ClickEvent e) => true;
506 protected override bool OnHover(HoverEvent e) => true;
507
508 public override bool AcceptsFocus => !TopLevelMenu;
509
510 public override bool RequestsFocus => !TopLevelMenu && State == MenuState.Open;
511
512 protected override void OnFocusLost(FocusLostEvent e)
513 {
514 // Case where a sub-menu was opened the focus will be transferred to that sub-menu while this menu will receive OnFocusLost
515 if (submenu?.State == MenuState.Open)
516 return;
517
518 if (!TopLevelMenu)
519 // At this point we should have lost focus due to clicks outside the menu structure
520 closeAll();
521 }
522
523 /// <summary>
524 /// Closes all open <see cref="Menu"/>s.
525 /// </summary>
526 private void closeAll()
527 {
528 Close();
529 parentMenu?.closeFromChild(triggeringItem);
530 }
531
532 private void closeFromChild(MenuItem source)
533 {
534 if (IsHovered || (parentMenu?.IsHovered ?? false)) return;
535
536 if (triggeringItem?.Items?.Contains(source) ?? triggeringItem == null)
537 {
538 Close();
539 parentMenu?.closeFromChild(triggeringItem);
540 }
541 }
542
543 #endregion
544
545 /// <summary>
546 /// Creates a sub-menu for <see cref="MenuItem.Items"/> of <see cref="MenuItem"/>s added to this <see cref="Menu"/>.
547 /// </summary>
548 protected abstract Menu CreateSubMenu();
549
550 /// <summary>
551 /// Creates the visual representation for a <see cref="MenuItem"/>.
552 /// </summary>
553 /// <param name="item">The <see cref="MenuItem"/> that is to be visualised.</param>
554 /// <returns>The visual representation.</returns>
555 protected abstract DrawableMenuItem CreateDrawableMenuItem(MenuItem item);
556
557 /// <summary>
558 /// Creates the <see cref="ScrollContainer{T}"/> to hold the items of this <see cref="Menu"/>.
559 /// </summary>
560 /// <param name="direction">The scrolling direction.</param>
561 /// <returns>The <see cref="ScrollContainer{T}"/>.</returns>
562 protected abstract ScrollContainer<Drawable> CreateScrollContainer(Direction direction);
563
564 #region DrawableMenuItem
565
566 // must be public due to mono bug(?) https://github.com/ppy/osu/issues/1204
567 public abstract class DrawableMenuItem : CompositeDrawable, IStateful<MenuItemState>
568 {
569 /// <summary>
570 /// Invoked when this <see cref="DrawableMenuItem"/>'s <see cref="State"/> changes.
571 /// </summary>
572 public event Action<MenuItemState> StateChanged;
573
574 /// <summary>
575 /// Invoked when this <see cref="DrawableMenuItem"/> is clicked. This occurs regardless of whether or not <see cref="MenuItem.Action"/> was
576 /// invoked or not, or whether <see cref="Item"/> contains any sub-<see cref="MenuItem"/>s.
577 /// </summary>
578 internal Action<DrawableMenuItem> Clicked;
579
580 /// <summary>
581 /// Invoked when this <see cref="DrawableMenuItem"/> is hovered. This runs one update frame behind the actual hover event.
582 /// </summary>
583 internal Action<DrawableMenuItem> Hovered;
584
585 /// <summary>
586 /// The <see cref="MenuItem"/> which this <see cref="DrawableMenuItem"/> represents.
587 /// </summary>
588 public readonly MenuItem Item;
589
590 /// <summary>
591 /// The content of this <see cref="DrawableMenuItem"/>, created through <see cref="CreateContent"/>.
592 /// </summary>
593 protected readonly Drawable Content;
594
595 /// <summary>
596 /// The background of this <see cref="DrawableMenuItem"/>.
597 /// </summary>
598 protected readonly Drawable Background;
599
600 /// <summary>
601 /// The foreground of this <see cref="DrawableMenuItem"/>. This contains the content of this <see cref="DrawableMenuItem"/>.
602 /// </summary>
603 protected readonly Container Foreground;
604
605 /// <summary>
606 /// Whether to close all menus when this action <see cref="DrawableMenuItem"/> is clicked.
607 /// </summary>
608 public virtual bool CloseMenuOnClick => true;
609
610 protected DrawableMenuItem(MenuItem item)
611 {
612 Item = item;
613
614 InternalChildren = new[]
615 {
616 Background = CreateBackground(),
617 Foreground = new Container
618 {
619 AutoSizeAxes = Axes.Both,
620 Child = Content = CreateContent()
621 },
622 };
623
624 if (Content is IHasText textContent)
625 {
626 textContent.Text = item.Text.Value;
627 Item.Text.ValueChanged += e => textContent.Text = e.NewValue;
628 }
629 }
630
631 /// <summary>
632 /// Sets various properties of this <see cref="DrawableMenuItem"/> that depend on the direction in which
633 /// <see cref="DrawableMenuItem"/>s flow inside the containing <see cref="Menu"/> (e.g. sizing axes).
634 /// </summary>
635 /// <param name="direction">The direction in which <see cref="DrawableMenuItem"/>s will be flowed.</param>
636 public virtual void SetFlowDirection(Direction direction)
637 {
638 RelativeSizeAxes = direction == Direction.Horizontal ? Axes.Y : Axes.X;
639 AutoSizeAxes = direction == Direction.Horizontal ? Axes.X : Axes.Y;
640 }
641
642 private Color4 backgroundColour = Color4.DarkSlateGray;
643
644 /// <summary>
645 /// Gets or sets the default background colour.
646 /// </summary>
647 public Color4 BackgroundColour
648 {
649 get => backgroundColour;
650 set
651 {
652 backgroundColour = value;
653 UpdateBackgroundColour();
654 }
655 }
656
657 private Color4 foregroundColour = Color4.White;
658
659 /// <summary>
660 /// Gets or sets the default foreground colour.
661 /// </summary>
662 public Color4 ForegroundColour
663 {
664 get => foregroundColour;
665 set
666 {
667 foregroundColour = value;
668 UpdateForegroundColour();
669 }
670 }
671
672 private Color4 backgroundColourHover = Color4.DarkGray;
673
674 /// <summary>
675 /// Gets or sets the background colour when this <see cref="DrawableMenuItem"/> is hovered.
676 /// </summary>
677 public Color4 BackgroundColourHover
678 {
679 get => backgroundColourHover;
680 set
681 {
682 backgroundColourHover = value;
683 UpdateBackgroundColour();
684 }
685 }
686
687 private Color4 foregroundColourHover = Color4.White;
688
689 /// <summary>
690 /// Gets or sets the foreground colour when this <see cref="DrawableMenuItem"/> is hovered.
691 /// </summary>
692 public Color4 ForegroundColourHover
693 {
694 get => foregroundColourHover;
695 set
696 {
697 foregroundColourHover = value;
698 UpdateForegroundColour();
699 }
700 }
701
702 private MenuItemState state;
703
704 public MenuItemState State
705 {
706 get => state;
707 set
708 {
709 state = value;
710
711 UpdateForegroundColour();
712 UpdateBackgroundColour();
713
714 StateChanged?.Invoke(state);
715 }
716 }
717
718 /// <summary>
719 /// The draw width of the text of this <see cref="DrawableMenuItem"/>.
720 /// </summary>
721 public float ContentDrawWidth => Content.DrawWidth;
722
723 /// <summary>
724 /// The draw width of the text of this <see cref="DrawableMenuItem"/>.
725 /// </summary>
726 public float ContentDrawHeight => Content.DrawHeight;
727
728 /// <summary>
729 /// Called after the <see cref="BackgroundColour"/> is modified or the hover state changes.
730 /// </summary>
731 protected virtual void UpdateBackgroundColour()
732 {
733 Background.FadeColour(IsHovered ? BackgroundColourHover : BackgroundColour);
734 }
735
736 /// <summary>
737 /// Called after the <see cref="ForegroundColour"/> is modified or the hover state changes.
738 /// </summary>
739 protected virtual void UpdateForegroundColour()
740 {
741 Foreground.FadeColour(IsHovered ? ForegroundColourHover : ForegroundColour);
742 }
743
744 protected override void LoadComplete()
745 {
746 base.LoadComplete();
747 Background.Colour = BackgroundColour;
748 Foreground.Colour = ForegroundColour;
749 }
750
751 protected override bool OnHover(HoverEvent e)
752 {
753 UpdateBackgroundColour();
754 UpdateForegroundColour();
755
756 Schedule(() =>
757 {
758 if (IsHovered)
759 Hovered?.Invoke(this);
760 });
761
762 return false;
763 }
764
765 protected override void OnHoverLost(HoverLostEvent e)
766 {
767 UpdateBackgroundColour();
768 UpdateForegroundColour();
769 base.OnHoverLost(e);
770 }
771
772 private bool hasSubmenu => Item.Items?.Count > 0;
773
774 protected override bool OnClick(ClickEvent e)
775 {
776 if (Item.Action.Disabled)
777 return true;
778
779 if (!hasSubmenu)
780 Item.Action.Value?.Invoke();
781
782 Clicked?.Invoke(this);
783
784 return true;
785 }
786
787 /// <summary>
788 /// Creates the background of this <see cref="DrawableMenuItem"/>.
789 /// </summary>
790 protected virtual Drawable CreateBackground() => new Box { RelativeSizeAxes = Axes.Both };
791
792 /// <summary>
793 /// Creates the content which will be displayed in this <see cref="DrawableMenuItem"/>.
794 /// If the <see cref="Drawable"/> returned implements <see cref="IHasText"/>, the text will be automatically
795 /// updated when the <see cref="MenuItem.Text"/> is updated.
796 /// </summary>
797 protected abstract Drawable CreateContent();
798 }
799
800 #endregion
801 }
802
803 public enum MenuState
804 {
805 Closed,
806 Open
807 }
808
809 public enum MenuItemState
810 {
811 NotSelected,
812 Selected
813 }
814}