A game framework written with osu! in mind.
at master 7.8 kB view raw
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}