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.Diagnostics;
7using System.Linq;
8using osu.Framework.Graphics;
9using osu.Framework.Input.Events;
10using osu.Framework.Input.States;
11using osu.Framework.Logging;
12using osuTK;
13using osuTK.Input;
14
15namespace osu.Framework.Input
16{
17 /// <summary>
18 /// Manages state and events (click, drag and double-click) for a single mouse button.
19 /// </summary>
20 public abstract class MouseButtonEventManager : ButtonEventManager<MouseButton>
21 {
22 /// <summary>
23 /// Used for requesting focus from click.
24 /// </summary>
25 internal Action<Drawable> RequestFocus;
26
27 /// <summary>
28 /// A function for retrieving the current time.
29 /// </summary>
30 internal Func<double> GetCurrentTime;
31
32 /// <summary>
33 /// Whether dragging is handled by the managed button.
34 /// </summary>
35 public abstract bool EnableDrag { get; }
36
37 /// <summary>
38 /// Whether click and double click are handled by the managed button.
39 /// </summary>
40 public abstract bool EnableClick { get; }
41
42 /// <summary>
43 /// Whether focus is changed when the button is clicked.
44 /// </summary>
45 public abstract bool ChangeFocusOnClick { get; }
46
47 protected MouseButtonEventManager(MouseButton button)
48 : base(button)
49 {
50 }
51
52 /// <summary>
53 /// The maximum time between two clicks for a double-click to be considered.
54 /// </summary>
55 public virtual float DoubleClickTime => 250;
56
57 /// <summary>
58 /// The distance that must be moved until a dragged click becomes invalid.
59 /// </summary>
60 public virtual float ClickDragDistance => 10;
61
62 /// <summary>
63 /// The position of the mouse when the last time the button is pressed.
64 /// </summary>
65 public Vector2? MouseDownPosition { get; protected set; }
66
67 /// <summary>
68 /// The time of last click.
69 /// </summary>
70 protected double? LastClickTime;
71
72 /// <summary>
73 /// The drawable which is clicked by the last click.
74 /// </summary>
75 protected WeakReference<Drawable> ClickedDrawable = new WeakReference<Drawable>(null);
76
77 /// <summary>
78 /// Whether a drag operation has started and <see cref="DraggedDrawable"/> has been searched for.
79 /// </summary>
80 protected bool DragStarted;
81
82 /// <summary>
83 /// The <see cref="Drawable"/> which is currently being dragged. null if none is.
84 /// </summary>
85 public Drawable DraggedDrawable { get; protected set; }
86
87 public void HandlePositionChange(InputState state, Vector2 lastPosition)
88 {
89 if (EnableDrag)
90 {
91 if (!DragStarted)
92 {
93 var mouse = state.Mouse;
94 if (mouse.IsPressed(Button) && Vector2Extensions.Distance(MouseDownPosition ?? mouse.Position, mouse.Position) > ClickDragDistance)
95 handleDragStart(state);
96 }
97
98 if (DragStarted)
99 handleDrag(state, lastPosition);
100 }
101 }
102
103 protected override Drawable HandleButtonDown(InputState state, List<Drawable> targets)
104 {
105 Trace.Assert(state.Mouse.IsPressed(Button));
106
107 if (state.Mouse.IsPositionValid)
108 MouseDownPosition = state.Mouse.Position;
109
110 Drawable handledBy = PropagateButtonEvent(targets, new MouseDownEvent(state, Button, MouseDownPosition));
111
112 if (LastClickTime != null && GetCurrentTime() - LastClickTime < DoubleClickTime)
113 {
114 if (handleDoubleClick(state, targets))
115 {
116 //when we handle a double-click we want to block a normal click from firing.
117 BlockNextClick = true;
118 LastClickTime = null;
119 }
120 }
121
122 return handledBy;
123 }
124
125 protected override void HandleButtonUp(InputState state, List<Drawable> targets)
126 {
127 Trace.Assert(!state.Mouse.IsPressed(Button));
128
129 if (targets != null)
130 PropagateButtonEvent(targets, new MouseUpEvent(state, Button, MouseDownPosition));
131
132 if (EnableClick && DraggedDrawable?.DragBlocksClick != true)
133 {
134 if (!BlockNextClick)
135 {
136 LastClickTime = GetCurrentTime();
137 handleClick(state, targets);
138 }
139 }
140
141 BlockNextClick = false;
142
143 if (EnableDrag)
144 handleDragEnd(state);
145
146 MouseDownPosition = null;
147 }
148
149 protected bool BlockNextClick;
150
151 private void handleClick(InputState state, List<Drawable> targets)
152 {
153 if (targets == null) return;
154
155 // due to the laziness of IEnumerable, .Where check should be done right before it is triggered for the event.
156 var drawables = targets.Intersect(InputQueue)
157 .Where(t => t.IsAlive && t.IsPresent && t.ReceivePositionalInputAt(state.Mouse.Position));
158
159 var clicked = PropagateButtonEvent(drawables, new ClickEvent(state, Button, MouseDownPosition));
160 ClickedDrawable.SetTarget(clicked);
161
162 if (ChangeFocusOnClick)
163 RequestFocus.Invoke(clicked);
164
165 if (clicked != null)
166 Logger.Log($"MouseClick handled by {clicked}.", LoggingTarget.Runtime, LogLevel.Debug);
167 }
168
169 private bool handleDoubleClick(InputState state, List<Drawable> targets)
170 {
171 if (!ClickedDrawable.TryGetTarget(out Drawable clicked))
172 return false;
173
174 if (!targets.Contains(clicked))
175 return false;
176
177 return PropagateButtonEvent(new[] { clicked }, new DoubleClickEvent(state, Button, MouseDownPosition)) != null;
178 }
179
180 private void handleDrag(InputState state, Vector2 lastPosition)
181 {
182 if (DraggedDrawable == null) return;
183
184 //Once a drawable is dragged, it remains in a dragged state until the drag is finished.
185 PropagateButtonEvent(new[] { DraggedDrawable }, new DragEvent(state, Button, MouseDownPosition, lastPosition));
186 }
187
188 private void handleDragStart(InputState state)
189 {
190 Trace.Assert(DraggedDrawable == null, $"The {nameof(DraggedDrawable)} was not set to null by {nameof(handleDragEnd)}.");
191 Trace.Assert(!DragStarted, $"A {nameof(DraggedDrawable)} was already searched for. Call {nameof(handleDragEnd)} first.");
192
193 Trace.Assert(MouseDownPosition != null);
194
195 DragStarted = true;
196
197 // also the laziness of IEnumerable here
198 var drawables = ButtonDownInputQueue.Where(t => t.IsAlive && t.IsPresent);
199
200 DraggedDrawable = PropagateButtonEvent(drawables, new DragStartEvent(state, Button, MouseDownPosition));
201 if (DraggedDrawable != null)
202 DraggedDrawable.IsDragged = true;
203 }
204
205 private void handleDragEnd(InputState state)
206 {
207 DragStarted = false;
208
209 if (DraggedDrawable == null) return;
210
211 var previousDragged = DraggedDrawable;
212 previousDragged.IsDragged = false;
213 DraggedDrawable = null;
214
215 PropagateButtonEvent(new[] { previousDragged }, new DragEndEvent(state, Button, MouseDownPosition));
216 }
217 }
218}