Skip to content

Commit

Permalink
Mouse Drag Distance Threshold
Browse files Browse the repository at this point in the history
- Added a mouse drag distance threshold value to allow tuning whether a small or accidental drag operation should still count towards a click event or not.
  • Loading branch information
juniordiscart committed Jun 16, 2024
1 parent 3440a7d commit 09d8d2f
Show file tree
Hide file tree
Showing 10 changed files with 79 additions and 24 deletions.
Binary file modified Assets/Impossible Odds/MouseEvents/Documentation.pdf
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ public bool IsDrag
public bool IsSingleClick => buttonState == MouseButtonEventType.SingleClick;

/// <summary>
/// Is this event a double-click event?
/// Is this event a double click event?
/// </summary>
public bool IsDoubleClick => buttonState == MouseButtonEventType.DoubleClick;

Expand Down
20 changes: 12 additions & 8 deletions Assets/Impossible Odds/MouseEvents/Runtime/MouseButtonEventType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,40 @@
/// <summary>
/// States a mouse button can reside in.
/// </summary>
public enum MouseButtonEventType : int
public enum MouseButtonEventType
{
/// <summary>
/// No current event of interest has happened.
/// </summary>
None = 0,
None,
/// <summary>
/// When the mouse button performed a single click, but is waiting for a follow-up action.
/// </summary>
SingleClickPending = 1,
SingleClickPending,
/// <summary>
/// When the mouse button performed a single click.
/// </summary>
SingleClick = 2,
SingleClick,
/// <summary>
/// When the mouse button performed a double click.
/// </summary>
DoubleClick = 3,
DoubleClick,
/// <summary>
/// When the mouse button is being dragged, but the threshold for a drag action has not been met yet.
/// </summary>
DragPending,
/// <summary>
/// When the mouse button is being used to initiate a drag action.
/// </summary>
DragStart = 4,
DragStart,
/// <summary>
/// When the mouse button is being used for dragging.
/// </summary>
Dragging = 5,
Dragging,
/// <summary>
/// When the mouse button completed a drag action.
/// </summary>
DragComplete = 6,
DragComplete,

Idle = None
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ public class MouseButtonStateTracker
public event Action<MouseButtonStateTracker> onStateUpdated;

private readonly MouseButton button;
private readonly Func<float> multiClickTimeThreshold = null;
private readonly Func<float> multiClickTimeThreshold;
private readonly Func<float> dragDistanceThreshold;

private MouseButtonEventType state = MouseButtonEventType.None;
private EventModifiers lastModifiers = EventModifiers.None;
Expand Down Expand Up @@ -42,10 +43,11 @@ public class MouseButtonStateTracker
/// </summary>
public Vector2 MousePosition => mousePosition;

public MouseButtonStateTracker(MouseButton button, Func<float> multiClickTimeThreshold)
public MouseButtonStateTracker(MouseButton button, Func<float> multiClickTimeThreshold, Func<float> dragDistanceThreshold)
{
this.button = button;
this.multiClickTimeThreshold = multiClickTimeThreshold;
this.dragDistanceThreshold = dragDistanceThreshold;
ClearState();
}

Expand Down Expand Up @@ -95,7 +97,7 @@ internal void ProcessEvent(Event mouseEvent)
internal void Suspend(Event mouseEvent)
{
// If the button doesn't match, don't bother.
if ((MouseButton)mouseEvent.button != (MouseButton)button)
if (mouseEvent.button != button)
{
return;
}
Expand Down Expand Up @@ -128,6 +130,7 @@ private void ProcessMouseUpEvent(Event mouseEvent)
switch (state)
{
case MouseButtonEventType.None:
case MouseButtonEventType.DragPending:
state = MouseButtonEventType.SingleClickPending;
lastModifiers = mouseEvent.modifiers;
mousePosition = mouseEvent.mousePosition;
Expand Down Expand Up @@ -163,19 +166,29 @@ private void ProcessMouseDragEvent(Event mouseEvent)
return;
}

// If a single click was still pending,
// then perform the click before getting to the drag operation.
// If a single click was still pending, then perform the click before getting to the drag operation.
if (state == MouseButtonEventType.SingleClickPending)
{
state = MouseButtonEventType.SingleClick;
lastClickAction = 0f;
}
else if ((state != MouseButtonEventType.DragStart) && (state != MouseButtonEventType.Dragging))
else if ((state != MouseButtonEventType.DragPending) &&
(state != MouseButtonEventType.DragStart) &&
(state != MouseButtonEventType.Dragging))
{
state = MouseButtonEventType.DragStart; // The first of such events initiates the drag start.
state = MouseButtonEventType.DragPending; // The first of such events declares that drag is pending, until the drag threshold is exceeded.
dragStartPosition = mouseEvent.mousePosition;
mousePosition = mouseEvent.mousePosition;
}
else if (state == MouseButtonEventType.DragPending)
{
if (Vector2.Distance(mouseEvent.mousePosition, dragStartPosition) > dragDistanceThreshold())
{
state = MouseButtonEventType.DragStart; // The first of such events initiates the drag start.
}

mousePosition = mouseEvent.mousePosition;
}
else
{
state = MouseButtonEventType.Dragging;
Expand Down
37 changes: 32 additions & 5 deletions Assets/Impossible Odds/MouseEvents/Runtime/MouseEventMonitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public class MouseEventMonitor : MonoBehaviour
private List<MouseButton> monitoredButtons = new List<MouseButton>();
[SerializeField, Tooltip("How long (in seconds) should it wait for registering a multi-click event?"), Range(0.1f, 1f)]
private float multiClickTimeThreshold = 0.3f;
[SerializeField, Tooltip("How far can the mouse move (in pixels) before it is considered a drag movement?"), Range(0, 50)]
private float dragDistanceThreshold = 0f;
[SerializeField, Tooltip("When the cursor is over UI, should the mouse event monitor suspend operations?")]
private bool suspendWhenOverUI;
[SerializeField, Tooltip("Should the cursor coordinates be reported in pixel coordinates (as reported by Input.mousePosition), or in GUI coordinates (as reported by OnGUI events)?")]
Expand Down Expand Up @@ -74,6 +76,23 @@ public float MultiClickTimeThreshold
}
}

/// <summary>
/// How far can the mouse cursor be dragged (in pixels) before it registers the start of a drag operation?
/// </summary>
public float DragDistanceThreshold
{
get => dragDistanceThreshold;
set
{
if (value < 0f)
{
throw new ArgumentOutOfRangeException(nameof(value), "The drag distance threshold should be greater than or equal to 0.");
}

dragDistanceThreshold = value;
}
}

/// <summary>
/// When the cursor is over UI, should the mouse event monitor suspend operations?
/// </summary>
Expand Down Expand Up @@ -160,12 +179,17 @@ public void StartMonitoring(MouseButton mouseButton)

monitoredButtons.Add(mouseButton);

if (enabled)
if (!enabled)
{
MouseButtonStateTracker stateTracker = new MouseButtonStateTracker(mouseButton, () => multiClickTimeThreshold);
stateTrackers[mouseButton] = stateTracker;
stateTracker.onStateUpdated += OnMouseKeyStateUpdate;
return;
}

MouseButtonStateTracker stateTracker = new MouseButtonStateTracker(
mouseButton,
() => multiClickTimeThreshold,
() => dragDistanceThreshold);
stateTrackers[mouseButton] = stateTracker;
stateTracker.onStateUpdated += OnMouseKeyStateUpdate;
}

/// <summary>
Expand All @@ -185,7 +209,10 @@ private void OnEnable()
{
foreach (MouseButton key in monitoredButtons)
{
MouseButtonStateTracker state = new MouseButtonStateTracker(key, () => multiClickTimeThreshold);
MouseButtonStateTracker state = new MouseButtonStateTracker(
key,
() => multiClickTimeThreshold,
() => dragDistanceThreshold);
stateTrackers[key] = state;
state.onStateUpdated += OnMouseKeyStateUpdate;
}
Expand Down
5 changes: 5 additions & 0 deletions Assets/Impossible Odds/MouseEvents/Samples/MouseEventsDemo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ private void OnMouseEvent(MouseButtonEvent mouseEvent)
break;
}

if (display != null)
{
Debug.Log(display.text);
}

// The dragging event is not send out each frame if there's not mouse movement.
// So don't clear it when this state is detected.
if (!mouseEvent.IsDragging && (display != null))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1747,6 +1747,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
monitoredButtons: 000000000200000001000000
multiClickTimeThreshold: 0.3
dragDistanceThreshold: 0
suspendWhenOverUI: 1
cursorPositionInPixelCoordinates: 1
--- !u!1 &1158115896
Expand Down Expand Up @@ -1973,7 +1974,7 @@ Camera:
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 2
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
m_BackGroundColor: {r: 0.35, g: 0.35, b: 0.35, a: 1}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
Expand Down
2 changes: 1 addition & 1 deletion Assets/Impossible Odds/MouseEvents/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "com.impossibleodds.mousevents",
"version": "1.2.0",
"version": "1.3.0",
"displayName": "Impossible Odds - Mouse Events",
"unity": "2019.4",
"documentationUrl": "https://github.com/juniordiscart/ImpossibleOdds-MouseEvents/blob/master/README.md",
Expand Down
Binary file modified Docs/Images/Demo_MouseEventMonitor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Additionally, you can also query the state of a specific mouse button on the mou

## Advanced

The main point of entry is the `MouseEventMonitor` script. It requires to be placed on a game object in your scene and will monitor the mouse inputs you set it up to be: left, right and/or middle mouse buttons. Apart from which mouse buttons it should monitor, you can also adjust the time threshold for registering multi-clicks (double click). Unity does not allow to transparently distinguish between single click and double click without always invoking the single click event. This multi-click time threshold is the time limit it will delay a single click event while listening in for a secondary click or other event.
The main point of entry is the `MouseEventMonitor` script. It requires to be placed on a game object in your scene and will monitor the mouse inputs you set it up to be: left, right and/or middle mouse buttons. Apart from which mouse buttons it should monitor, you can also adjust the time threshold for registering multi-clicks (double click). Unity does not allow to transparently distinguish between single click and double click without always invoking the single click event. This multi-click time threshold is the time limit it will delay a single click event while listening in for a secondary click or other event. Additionally, you can also set a drag distance threshold for when you want to let small or accidental drag operations to still count as a click event instead.

You can listen for events of the registered mouse buttons as well as querying the current state of a particular button using the `CurrentEvent` method. When a new mouse button requires monitoring, you can add it using the `StartMonitoring` method. Conversely, you can also stop monitoring events for a specific mouse button by calling the `StopMonitoring` method.

Expand Down Expand Up @@ -126,6 +126,11 @@ This package is provided under the [MIT][License] license.

* Fixed a runtime issue where clicks would be invalidated due to using Unity's built-in `Event` struct. This structure would put the event type to `Ignore` while it was actually a mouse button up/down event.

### v1.3.0

* Added a `DragDistanceThreshold` to the the `MouseEventMonitor` to allow small/accidental drag operations to still register as a click.
* Updated the sample scene to also log the performed actions to the console.

[License]: ./LICENSE.md
[Changelog]: ./CHANGELOG.md
[Logo]: ./Docs/Images/ImpossibleOddsLogo.png
Expand Down

0 comments on commit 09d8d2f

Please sign in to comment.