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.Diagnostics;
6using System.Linq;
7using osuTK;
8using osuTK.Input;
9using osu.Framework.Graphics.Containers;
10using osu.Framework.Graphics.UserInterface;
11using osu.Framework.Input;
12using osu.Framework.Input.Events;
13
14namespace osu.Framework.Graphics.Cursor
15{
16 /// <summary>
17 /// A container which manages a <see cref="Menu"/>.
18 /// If a right-click happens on a <see cref="Drawable"/> that implements <see cref="IHasContextMenu"/> and exists as a child of the same <see cref="InputManager"/> as this container,
19 /// a <see cref="Menu"/> will be displayed with bottom-right origin at the right-clicked position.
20 /// </summary>
21 public abstract class ContextMenuContainer : CursorEffectContainer<ContextMenuContainer, IHasContextMenu>
22 {
23 private readonly Menu menu;
24
25 private IHasContextMenu menuTarget;
26 private Vector2 targetRelativePosition;
27
28 /// <summary>
29 /// Creates a new context menu. Can be overridden to supply custom subclass of <see cref="Menu"/>.
30 /// </summary>
31 protected abstract Menu CreateMenu();
32
33 private readonly Container content;
34
35 protected override Container<Drawable> Content => content;
36
37 /// <summary>
38 /// Creates a new <see cref="ContextMenuContainer"/>.
39 /// </summary>
40 protected ContextMenuContainer()
41 {
42 AddInternal(content = new Container
43 {
44 RelativeSizeAxes = Axes.Both,
45 });
46
47 AddInternal(menu = CreateMenu());
48 }
49
50 protected override void OnSizingChanged()
51 {
52 base.OnSizingChanged();
53
54 if (content != null)
55 {
56 // reset to none to prevent exceptions
57 content.RelativeSizeAxes = Axes.None;
58 content.AutoSizeAxes = Axes.None;
59
60 // in addition to using this.RelativeSizeAxes, sets RelativeSizeAxes on every axis that is neither relative size nor auto size
61 content.RelativeSizeAxes = Axes.Both & ~AutoSizeAxes;
62 content.AutoSizeAxes = AutoSizeAxes;
63 }
64 }
65
66 protected override bool OnMouseDown(MouseDownEvent e)
67 {
68 switch (e.Button)
69 {
70 case MouseButton.Right:
71 var (target, items) = FindTargets()
72 .Select(t => (target: t, items: t.ContextMenuItems))
73 .FirstOrDefault(result => result.items != null);
74
75 menuTarget = target;
76
77 if (menuTarget == null || items.Length == 0)
78 {
79 if (menu.State == MenuState.Open)
80 menu.Close();
81 return false;
82 }
83
84 menu.Items = items;
85
86 targetRelativePosition = menuTarget.ToLocalSpace(e.ScreenSpaceMousePosition);
87
88 menu.Open();
89 return true;
90
91 default:
92 cancelDisplay();
93 return false;
94 }
95 }
96
97 private void cancelDisplay()
98 {
99 Debug.Assert(menu != null);
100
101 menu.Close();
102 menuTarget = null;
103 }
104
105 protected override void UpdateAfterChildren()
106 {
107 base.UpdateAfterChildren();
108
109 if (menu.State != MenuState.Open || menuTarget == null) return;
110
111 if ((menuTarget as Drawable)?.FindClosestParent<ContextMenuContainer>() != this || (!menuTarget?.IsPresent ?? false))
112 {
113 cancelDisplay();
114 return;
115 }
116
117 Vector2 pos = menuTarget.ToSpaceOfOtherDrawable(targetRelativePosition, this);
118
119 Vector2 overflow = pos + menu.DrawSize - DrawSize;
120
121 if (overflow.X > 0)
122 pos.X -= Math.Clamp(overflow.X, 0, menu.DrawWidth);
123 if (overflow.Y > 0)
124 pos.Y -= Math.Clamp(overflow.Y, 0, menu.DrawHeight);
125
126 if (pos.X < 0)
127 pos.X += Math.Clamp(-pos.X, 0, menu.DrawWidth);
128 if (pos.Y < 0)
129 pos.Y += Math.Clamp(-pos.Y, 0, menu.DrawHeight);
130
131 menu.Position = pos;
132 }
133 }
134}