mirror of
https://github.com/SK-la/osu-framework.git
synced 2026-03-13 11:20:31 +00:00
1129 lines
47 KiB
C#
1129 lines
47 KiB
C#
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
// See the LICENCE file in the repository root for full licence text.
|
|
|
|
#nullable disable
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Diagnostics;
|
|
using JetBrains.Annotations;
|
|
using osu.Framework.Allocation;
|
|
using osu.Framework.Extensions.ListExtensions;
|
|
using osu.Framework.Graphics;
|
|
using osu.Framework.Graphics.Containers;
|
|
using osu.Framework.Input.Events;
|
|
using osu.Framework.Input.Handlers;
|
|
using osu.Framework.Input.StateChanges;
|
|
using osu.Framework.Input.StateChanges.Events;
|
|
using osu.Framework.Input.States;
|
|
using osu.Framework.Lists;
|
|
using osu.Framework.Logging;
|
|
using osu.Framework.Platform;
|
|
using osu.Framework.Statistics;
|
|
using osu.Framework.Threading;
|
|
using osuTK;
|
|
using osuTK.Input;
|
|
using JoystickState = osu.Framework.Input.States.JoystickState;
|
|
using KeyboardState = osu.Framework.Input.States.KeyboardState;
|
|
using MouseState = osu.Framework.Input.States.MouseState;
|
|
|
|
namespace osu.Framework.Input
|
|
{
|
|
public abstract partial class InputManager : Container, IInputStateChangeHandler, IFocusManager
|
|
{
|
|
/// <summary>
|
|
/// The initial delay before key repeat begins.
|
|
/// </summary>
|
|
private const int repeat_initial_delay = 250;
|
|
|
|
/// <summary>
|
|
/// The delay between key repeats after the initial repeat.
|
|
/// </summary>
|
|
private const int repeat_tick_rate = 70;
|
|
|
|
[Resolved(CanBeNull = true)]
|
|
protected GameHost Host { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The currently focused <see cref="Drawable"/>. Null if there is no current focus.
|
|
/// </summary>
|
|
public Drawable FocusedDrawable { get; internal set; }
|
|
|
|
/// <summary>
|
|
/// Any drawable that was focused directly via <see cref="ChangeFocus(Drawable, InputState)"/> during the handling of a click,
|
|
/// and <i>not</i> as a result of the automatic post-process change of focus from the click.
|
|
/// </summary>
|
|
internal Drawable FocusedDrawableThisClick;
|
|
|
|
protected abstract ImmutableArray<InputHandler> InputHandlers { get; }
|
|
|
|
private double keyboardRepeatTime;
|
|
private Key? keyboardRepeatKey;
|
|
|
|
/// <summary>
|
|
/// The initial input state. <see cref="CurrentState"/> is always equal (as a reference) to the value returned from this.
|
|
/// </summary>
|
|
protected virtual InputState CreateInitialState() => new InputState(
|
|
new MouseState { IsPositionValid = false },
|
|
new KeyboardState(),
|
|
new TouchState(),
|
|
new JoystickState(),
|
|
new MidiState(),
|
|
new TabletState()
|
|
);
|
|
|
|
/// <summary>
|
|
/// The last processed state.
|
|
/// </summary>
|
|
public readonly InputState CurrentState;
|
|
|
|
/// <summary>
|
|
/// The <see cref="Drawable"/> which is currently being dragged. null if none is.
|
|
/// </summary>
|
|
public Drawable DraggedDrawable
|
|
{
|
|
get
|
|
{
|
|
mouseButtonEventManagers.TryGetValue(MouseButton.Left, out var manager);
|
|
return manager?.DraggedDrawable;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Contains the previously hovered <see cref="Drawable"/>s prior to when
|
|
/// <see cref="hoveredDrawables"/> got updated.
|
|
/// </summary>
|
|
private readonly List<Drawable> lastHoveredDrawables = new List<Drawable>();
|
|
|
|
/// <summary>
|
|
/// Contains all hovered <see cref="Drawable"/>s in top-down order up to the first
|
|
/// which returned true in its <see cref="Drawable.OnHover"/> method.
|
|
/// Top-down in this case means reverse draw order, i.e. the front-most visible
|
|
/// <see cref="Drawable"/> first, and <see cref="Container"/>s after their children.
|
|
/// </summary>
|
|
private readonly List<Drawable> hoveredDrawables = new List<Drawable>();
|
|
|
|
/// <summary>
|
|
/// The <see cref="Drawable"/> which returned true in its
|
|
/// <see cref="Drawable.OnHover"/> method, or null if none did so.
|
|
/// </summary>
|
|
private Drawable hoverHandledDrawable;
|
|
|
|
/// <summary>
|
|
/// Contains all hovered <see cref="Drawable"/>s in top-down order up to the first
|
|
/// which returned true in its <see cref="Drawable.OnHover"/> method.
|
|
/// Top-down in this case means reverse draw order, i.e. the front-most visible
|
|
/// <see cref="Drawable"/> first, and <see cref="Container"/>s after their children.
|
|
/// </summary>
|
|
public SlimReadOnlyListWrapper<Drawable> HoveredDrawables => hoveredDrawables.AsSlimReadOnly();
|
|
|
|
/// <summary>
|
|
/// Contains all <see cref="Drawable"/>s in top-down order which are considered
|
|
/// for positional input. This list is the same as <see cref="HoveredDrawables"/>, only
|
|
/// that the return value of <see cref="Drawable.OnHover"/> is not taken
|
|
/// into account.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This collection should not be retained as a reference. The contents is not stable outside of local usage.
|
|
/// </remarks>
|
|
public SlimReadOnlyListWrapper<Drawable> PositionalInputQueue => buildPositionalInputQueue(CurrentState.Mouse.Position);
|
|
|
|
/// <summary>
|
|
/// Contains all <see cref="Drawable"/>s in top-down order which are considered
|
|
/// for non-positional input.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This collection should not be retained as a reference. The contents is not stable outside of local usage.
|
|
/// </remarks>
|
|
public SlimReadOnlyListWrapper<Drawable> NonPositionalInputQueue => buildNonPositionalInputQueue();
|
|
|
|
private readonly Dictionary<MouseButton, MouseButtonEventManager> mouseButtonEventManagers = new Dictionary<MouseButton, MouseButtonEventManager>();
|
|
private readonly Dictionary<Key, KeyEventManager> keyButtonEventManagers = new Dictionary<Key, KeyEventManager>();
|
|
private readonly Dictionary<TouchSource, TouchEventManager> touchEventManagers = new Dictionary<TouchSource, TouchEventManager>();
|
|
private readonly Dictionary<TabletPenButton, TabletPenButtonEventManager> tabletPenButtonEventManagers = new Dictionary<TabletPenButton, TabletPenButtonEventManager>();
|
|
|
|
private readonly Dictionary<TabletAuxiliaryButton, TabletAuxiliaryButtonEventManager> tabletAuxiliaryButtonEventManagers =
|
|
new Dictionary<TabletAuxiliaryButton, TabletAuxiliaryButtonEventManager>();
|
|
|
|
private readonly Dictionary<JoystickButton, JoystickButtonEventManager> joystickButtonEventManagers = new Dictionary<JoystickButton, JoystickButtonEventManager>();
|
|
private readonly Dictionary<MidiKey, MidiKeyEventManager> midiKeyEventManagers = new Dictionary<MidiKey, MidiKeyEventManager>();
|
|
|
|
private readonly Dictionary<JoystickAxisSource, JoystickAxisEventManager> joystickAxisEventManagers = new Dictionary<JoystickAxisSource, JoystickAxisEventManager>();
|
|
|
|
/// <summary>
|
|
/// Whether to produce mouse input on any touch input from latest source.
|
|
/// </summary>
|
|
protected virtual bool MapMouseToLatestTouch => true;
|
|
|
|
/// <summary>
|
|
/// Whether long touches should produce right mouse click, if mouse is mapped to touch.
|
|
/// </summary>
|
|
protected virtual bool AllowRightClickFromLongTouch => true;
|
|
|
|
protected InputManager()
|
|
{
|
|
CurrentState = CreateInitialState();
|
|
RelativeSizeAxes = Axes.Both;
|
|
|
|
foreach (var button in Enum.GetValues<MouseButton>())
|
|
{
|
|
var manager = CreateButtonEventManagerFor(button);
|
|
|
|
manager.InputManager = this;
|
|
manager.GetInputQueue = () => PositionalInputQueue;
|
|
|
|
mouseButtonEventManagers.Add(button, manager);
|
|
}
|
|
}
|
|
|
|
protected override void LoadComplete()
|
|
{
|
|
base.LoadComplete();
|
|
|
|
// Set mouse position to zero in input manager local space instead of screen space zero.
|
|
// This ensures initial mouse position is non-negative in nested input managers whose origin is not (0, 0).
|
|
CurrentState.Mouse.Position = ToScreenSpace(Vector2.Zero);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a <see cref="MouseButtonEventManager"/> for a specified mouse button.
|
|
/// </summary>
|
|
/// <param name="button">The button to be handled by the returned manager.</param>
|
|
/// <returns>The <see cref="MouseButtonEventManager"/>.</returns>
|
|
protected virtual MouseButtonEventManager CreateButtonEventManagerFor(MouseButton button)
|
|
{
|
|
switch (button)
|
|
{
|
|
case MouseButton.Left:
|
|
return new MouseLeftButtonEventManager(button);
|
|
|
|
default:
|
|
return new MouseMinorButtonEventManager(button);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the <see cref="MouseButtonEventManager"/> responsible for a specified mouse button.
|
|
/// </summary>
|
|
/// <param name="button">The button to find the manager for.</param>
|
|
/// <returns>The <see cref="MouseButtonEventManager"/>.</returns>
|
|
public MouseButtonEventManager GetButtonEventManagerFor(MouseButton button) =>
|
|
mouseButtonEventManagers.GetValueOrDefault(button);
|
|
|
|
/// <summary>
|
|
/// Create a <see cref="KeyEventManager"/> for a specified key.
|
|
/// </summary>
|
|
/// <param name="key">The key to be handled by the returned manager.</param>
|
|
/// <returns>The <see cref="KeyEventManager"/>.</returns>
|
|
protected virtual KeyEventManager CreateButtonEventManagerFor(Key key) => new KeyEventManager(key);
|
|
|
|
/// <summary>
|
|
/// Get the <see cref="KeyEventManager"/> responsible for a specified key.
|
|
/// </summary>
|
|
/// <param name="key">The key to find the manager for.</param>
|
|
/// <returns>The <see cref="KeyEventManager"/>.</returns>
|
|
public KeyEventManager GetButtonEventManagerFor(Key key)
|
|
{
|
|
if (keyButtonEventManagers.TryGetValue(key, out var existing))
|
|
return existing;
|
|
|
|
var manager = CreateButtonEventManagerFor(key);
|
|
manager.InputManager = this;
|
|
manager.GetInputQueue = () => NonPositionalInputQueue;
|
|
return keyButtonEventManagers[key] = manager;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a <see cref="TouchEventManager"/> for a specified touch source.
|
|
/// </summary>
|
|
/// <param name="source">The touch source to be handled by the returned manager.</param>
|
|
/// <returns>The <see cref="TouchEventManager"/>.</returns>
|
|
protected virtual TouchEventManager CreateButtonEventManagerFor(TouchSource source) => new TouchEventManager(source);
|
|
|
|
/// <summary>
|
|
/// Get the <see cref="TouchEventManager"/> responsible for a specified touch source.
|
|
/// </summary>
|
|
/// <param name="source">The touch source to find the manager for.</param>
|
|
/// <returns>The <see cref="TouchEventManager"/>.</returns>
|
|
public TouchEventManager GetButtonEventManagerFor(TouchSource source)
|
|
{
|
|
if (touchEventManagers.TryGetValue(source, out var existing))
|
|
return existing;
|
|
|
|
var manager = CreateButtonEventManagerFor(source);
|
|
manager.InputManager = this;
|
|
manager.GetInputQueue = () => buildPositionalInputQueue(CurrentState.Touch.TouchPositions[(int)source]);
|
|
return touchEventManagers[source] = manager;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a <see cref="TabletPenButtonEventManager"/> for a specified tablet pen button.
|
|
/// </summary>
|
|
/// <param name="button">The button to be handled by the returned manager.</param>
|
|
/// <returns>The <see cref="TabletPenButtonEventManager"/>.</returns>
|
|
protected virtual TabletPenButtonEventManager CreateButtonEventManagerFor(TabletPenButton button) => new TabletPenButtonEventManager(button);
|
|
|
|
/// <summary>
|
|
/// Get the <see cref="TabletPenButtonEventManager"/> responsible for a specified tablet pen button.
|
|
/// </summary>
|
|
/// <param name="button">The button to find the manager for.</param>
|
|
/// <returns>The <see cref="TabletPenButtonEventManager"/>.</returns>
|
|
public TabletPenButtonEventManager GetButtonEventManagerFor(TabletPenButton button)
|
|
{
|
|
if (tabletPenButtonEventManagers.TryGetValue(button, out var existing))
|
|
return existing;
|
|
|
|
var manager = CreateButtonEventManagerFor(button);
|
|
manager.InputManager = this;
|
|
manager.GetInputQueue = () => PositionalInputQueue;
|
|
return tabletPenButtonEventManagers[button] = manager;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a <see cref="TabletAuxiliaryButtonEventManager"/> for a specified tablet auxiliary button.
|
|
/// </summary>
|
|
/// <param name="button">The button to be handled by the returned manager.</param>
|
|
/// <returns>The <see cref="TabletAuxiliaryButtonEventManager"/>.</returns>
|
|
protected virtual TabletAuxiliaryButtonEventManager CreateButtonEventManagerFor(TabletAuxiliaryButton button) => new TabletAuxiliaryButtonEventManager(button);
|
|
|
|
/// <summary>
|
|
/// Get the <see cref="TabletAuxiliaryButtonEventManager"/> responsible for a specified tablet auxiliary button.
|
|
/// </summary>
|
|
/// <param name="button">The button to find the manager for.</param>
|
|
/// <returns>The <see cref="TabletAuxiliaryButtonEventManager"/>.</returns>
|
|
public TabletAuxiliaryButtonEventManager GetButtonEventManagerFor(TabletAuxiliaryButton button)
|
|
{
|
|
if (tabletAuxiliaryButtonEventManagers.TryGetValue(button, out var existing))
|
|
return existing;
|
|
|
|
var manager = CreateButtonEventManagerFor(button);
|
|
manager.InputManager = this;
|
|
manager.GetInputQueue = () => NonPositionalInputQueue;
|
|
return tabletAuxiliaryButtonEventManagers[button] = manager;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a <see cref="JoystickButtonEventManager"/> for a specified joystick button.
|
|
/// </summary>
|
|
/// <param name="button">The button to be handled by the returned manager.</param>
|
|
/// <returns>The <see cref="JoystickButtonEventManager"/>.</returns>
|
|
protected virtual JoystickButtonEventManager CreateButtonEventManagerFor(JoystickButton button) => new JoystickButtonEventManager(button);
|
|
|
|
/// <summary>
|
|
/// Get the <see cref="JoystickButtonEventManager"/> responsible for a specified joystick button.
|
|
/// </summary>
|
|
/// <param name="button">The button to find the manager for.</param>
|
|
/// <returns>The <see cref="JoystickButtonEventManager"/>.</returns>
|
|
public JoystickButtonEventManager GetButtonEventManagerFor(JoystickButton button)
|
|
{
|
|
if (joystickButtonEventManagers.TryGetValue(button, out var existing))
|
|
return existing;
|
|
|
|
var manager = CreateButtonEventManagerFor(button);
|
|
manager.InputManager = this;
|
|
manager.GetInputQueue = () => NonPositionalInputQueue;
|
|
return joystickButtonEventManagers[button] = manager;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a <see cref="MidiKeyEventManager"/> for a specified midi key.
|
|
/// </summary>
|
|
/// <param name="key">The key to be handled by the returned manager.</param>
|
|
/// <returns>The <see cref="MidiKeyEventManager"/>.</returns>
|
|
protected virtual MidiKeyEventManager CreateButtonEventManagerFor(MidiKey key) => new MidiKeyEventManager(key);
|
|
|
|
/// <summary>
|
|
/// Get the <see cref="MidiKeyEventManager"/> responsible for a specified midi key.
|
|
/// </summary>
|
|
/// <param name="key">The key to find the manager for.</param>
|
|
/// <returns>The <see cref="MidiKeyEventManager"/>.</returns>
|
|
public MidiKeyEventManager GetButtonEventManagerFor(MidiKey key)
|
|
{
|
|
if (midiKeyEventManagers.TryGetValue(key, out var existing))
|
|
return existing;
|
|
|
|
var manager = CreateButtonEventManagerFor(key);
|
|
manager.InputManager = this;
|
|
manager.GetInputQueue = () => NonPositionalInputQueue;
|
|
return midiKeyEventManagers[key] = manager;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a <see cref="JoystickAxisEventManager"/> for a specified joystick axis.
|
|
/// </summary>
|
|
/// <param name="source">The axis to be handled by the returned manager.</param>
|
|
/// <returns>The <see cref="JoystickAxisEventManager"/>.</returns>
|
|
protected virtual JoystickAxisEventManager CreateJoystickAxisEventManagerFor(JoystickAxisSource source) => new JoystickAxisEventManager(source);
|
|
|
|
/// <summary>
|
|
/// Get the <see cref="JoystickAxisEventManager"/> responsible for a specified joystick axis.
|
|
/// </summary>
|
|
/// <param name="source">The axis to find the manager for.</param>
|
|
/// <returns>The <see cref="JoystickAxisEventManager"/>.</returns>
|
|
public JoystickAxisEventManager GetJoystickAxisEventManagerFor(JoystickAxisSource source)
|
|
{
|
|
if (joystickAxisEventManagers.TryGetValue(source, out var existing))
|
|
return existing;
|
|
|
|
var manager = CreateJoystickAxisEventManagerFor(source);
|
|
manager.GetInputQueue = () => NonPositionalInputQueue;
|
|
return joystickAxisEventManagers[source] = manager;
|
|
}
|
|
|
|
[Obsolete("This method does not allow trapping focus. Use GetContainingFocusManager().TriggerFocusContention instead.")] // Can be removed 20241118
|
|
public void TriggerFocusContention(Drawable triggerSource)
|
|
{
|
|
if (FocusedDrawable == null) return;
|
|
|
|
Logger.Log($"Focus contention triggered by {triggerSource}.");
|
|
changeFocus(null);
|
|
}
|
|
|
|
[Obsolete("This method does not allow trapping focus. Use GetContainingFocusManager().ChangeFocus() instead.")] // Can be removed 20241118
|
|
public bool ChangeFocus(Drawable potentialFocusTarget) => changeFocus(potentialFocusTarget);
|
|
|
|
/// <summary>
|
|
/// Changes the currently-focused drawable. First checks that <paramref name="potentialFocusTarget"/> is in a valid state to receive focus,
|
|
/// then unfocuses the current <see cref="FocusedDrawable"/> and focuses <paramref name="potentialFocusTarget"/>.
|
|
/// <paramref name="potentialFocusTarget"/> can be null to reset focus.
|
|
/// If the given drawable is already focused, nothing happens and no events are fired.
|
|
/// </summary>
|
|
/// <param name="potentialFocusTarget">The drawable to become focused.</param>
|
|
/// <returns>True if the given drawable is now focused (or focus is dropped in the case of a null target).</returns>
|
|
private bool changeFocus(Drawable potentialFocusTarget) => ChangeFocus(potentialFocusTarget, CurrentState);
|
|
|
|
/// <summary>
|
|
/// Changes the currently-focused drawable. First checks that <paramref name="potentialFocusTarget"/> is in a valid state to receive focus,
|
|
/// then unfocuses the current <see cref="FocusedDrawable"/> and focuses <paramref name="potentialFocusTarget"/>.
|
|
/// <paramref name="potentialFocusTarget"/> can be null to reset focus.
|
|
/// If the given drawable is already focused, nothing happens and no events are fired.
|
|
/// </summary>
|
|
/// <param name="potentialFocusTarget">The drawable to become focused.</param>
|
|
/// <param name="state">The <see cref="InputState"/> associated with the focusing event.</param>
|
|
/// <returns>True if the given drawable is now focused (or focus is dropped in the case of a null target).</returns>
|
|
protected bool ChangeFocus(Drawable potentialFocusTarget, InputState state)
|
|
{
|
|
if (potentialFocusTarget == FocusedDrawable)
|
|
{
|
|
FocusedDrawableThisClick = FocusedDrawable;
|
|
return true;
|
|
}
|
|
|
|
if (potentialFocusTarget != null && (!isDrawableValidForFocus(potentialFocusTarget) || !potentialFocusTarget.AcceptsFocus))
|
|
return false;
|
|
|
|
var previousFocus = FocusedDrawable;
|
|
|
|
FocusedDrawable = null;
|
|
|
|
if (previousFocus != null)
|
|
{
|
|
previousFocus.HasFocus = false;
|
|
previousFocus.TriggerEvent(new FocusLostEvent(state, potentialFocusTarget));
|
|
|
|
if (FocusedDrawable != null) throw new InvalidOperationException($"Focus cannot be changed inside {nameof(OnFocusLost)}");
|
|
}
|
|
|
|
FocusedDrawable = potentialFocusTarget;
|
|
|
|
Logger.Log($"Focus changed from {previousFocus?.ToString() ?? "nothing"} to {FocusedDrawable?.ToString() ?? "nothing"}.", LoggingTarget.Runtime, LogLevel.Debug);
|
|
|
|
if (FocusedDrawable != null)
|
|
{
|
|
FocusedDrawable.HasFocus = true;
|
|
FocusedDrawable.TriggerEvent(new FocusEvent(state, previousFocus));
|
|
}
|
|
|
|
FocusedDrawableThisClick = FocusedDrawable;
|
|
|
|
return true;
|
|
}
|
|
|
|
internal override bool BuildNonPositionalInputQueue(List<Drawable> queue, bool allowBlocking = true)
|
|
{
|
|
if (!allowBlocking)
|
|
base.BuildNonPositionalInputQueue(queue, false);
|
|
|
|
return false;
|
|
}
|
|
|
|
internal override bool BuildPositionalInputQueue(Vector2 screenSpacePos, List<Drawable> queue) => false;
|
|
|
|
private bool hoverEventsUpdated;
|
|
|
|
private readonly List<Drawable> highFrequencyDrawables = new List<Drawable>();
|
|
|
|
private MouseMoveEvent lastMouseMove;
|
|
|
|
protected override void Update()
|
|
{
|
|
unfocusIfNoLongerValid();
|
|
|
|
// aggressively clear to avoid holding references.
|
|
inputQueue.Clear();
|
|
positionalInputQueue.Clear();
|
|
|
|
hoverEventsUpdated = false;
|
|
|
|
var pendingInputs = GetPendingInputs();
|
|
|
|
if (pendingInputs.Count > 0)
|
|
lastMouseMove = null;
|
|
|
|
foreach (var result in pendingInputs)
|
|
result.Apply(CurrentState, this);
|
|
|
|
if (CurrentState.Mouse.IsPositionValid)
|
|
{
|
|
Debug.Assert(highFrequencyDrawables.Count == 0);
|
|
|
|
foreach (var d in PositionalInputQueue)
|
|
{
|
|
if (d is IRequireHighFrequencyMousePosition)
|
|
highFrequencyDrawables.Add(d);
|
|
}
|
|
|
|
if (highFrequencyDrawables.Count > 0)
|
|
{
|
|
// conditional avoid allocs of MouseMoveEvent when state is guaranteed to not have been mutated.
|
|
// can be removed if we pool/change UIEvent allocation to be more efficient.
|
|
lastMouseMove ??= new MouseMoveEvent(CurrentState);
|
|
|
|
PropagateBlockableEvent(highFrequencyDrawables.AsSlimReadOnly(), lastMouseMove);
|
|
}
|
|
|
|
highFrequencyDrawables.Clear();
|
|
}
|
|
|
|
updateKeyRepeat(CurrentState);
|
|
|
|
// Other inputs or drawable changes may affect hover even if
|
|
// there were no mouse movements, so it must be updated every frame.
|
|
if (!hoverEventsUpdated)
|
|
updateHoverEvents(CurrentState);
|
|
|
|
if (FocusedDrawable == null)
|
|
focusTopMostRequestingDrawable();
|
|
|
|
if (!AllowRightClickFromLongTouch && touchLongPressDelegate != null)
|
|
cancelTouchLongPress();
|
|
|
|
base.Update();
|
|
}
|
|
|
|
private void updateKeyRepeat(InputState state)
|
|
{
|
|
if (!(keyboardRepeatKey is Key key)) return;
|
|
|
|
keyboardRepeatTime -= Time.Elapsed;
|
|
|
|
while (keyboardRepeatTime < 0)
|
|
{
|
|
GetButtonEventManagerFor(key).HandleRepeat(state);
|
|
keyboardRepeatTime += repeat_tick_rate;
|
|
}
|
|
}
|
|
|
|
private readonly List<IInput> inputs = new List<IInput>();
|
|
|
|
private readonly List<IInput> dequeuedInputs = new List<IInput>();
|
|
|
|
private InputHandler mouseSource;
|
|
|
|
private double mouseSourceDebounceTimeRemaining;
|
|
|
|
private double lastPendingInputRetrievalTime;
|
|
|
|
/// <summary>
|
|
/// The length in time after which a lower priority input handler is allowed to take over mouse control from a high priority handler that is no longer reporting.
|
|
/// It's safe to assume that all input devices are reporting at higher than the debounce time specified here (20hz).
|
|
/// If the delay seen between device swaps is ever considered to be too slow, this can likely be further increased up to 100hz.
|
|
/// </summary>
|
|
private const int mouse_source_debounce_time = 50;
|
|
|
|
protected virtual List<IInput> GetPendingInputs()
|
|
{
|
|
double now = Clock.CurrentTime;
|
|
double elapsed = now - lastPendingInputRetrievalTime;
|
|
lastPendingInputRetrievalTime = now;
|
|
|
|
inputs.Clear();
|
|
|
|
bool reachedPreviousMouseSource = false;
|
|
|
|
foreach (var h in InputHandlers)
|
|
{
|
|
if (!h.IsActive)
|
|
continue;
|
|
|
|
dequeuedInputs.Clear();
|
|
h.CollectPendingInputs(dequeuedInputs);
|
|
|
|
foreach (var i in dequeuedInputs)
|
|
{
|
|
// To avoid the same device reporting via two channels (and causing feedback), only one handler should be allowed to
|
|
// report mouse position data at a time. Handlers are given priority based on their constructed order.
|
|
// Devices which higher priority are allowed to take over control immediately, after which a delay is enforced (on every subsequent positional report)
|
|
// before a lower priority device can obtain control.
|
|
if (i is MousePositionAbsoluteInput || i is MousePositionRelativeInput)
|
|
{
|
|
if (mouseSource == null // a new device taking control when no existing preference is present.
|
|
|| mouseSource == h // if this is the device which currently has control, renew the debounce delay.
|
|
|| !reachedPreviousMouseSource // we have not reached the previous mouse source, so a higher priority device can take over control.
|
|
|| mouseSourceDebounceTimeRemaining <= 0) // a lower priority device taking over control if the debounce delay has elapsed.
|
|
{
|
|
mouseSource = h;
|
|
mouseSourceDebounceTimeRemaining = mouse_source_debounce_time;
|
|
}
|
|
else
|
|
{
|
|
// drop positional input if we did not meet the criteria to be the current reporting handler.
|
|
continue;
|
|
}
|
|
}
|
|
|
|
inputs.Add(i);
|
|
}
|
|
|
|
// track whether we have passed the handler which is currently in control of positional handling.
|
|
// importantly, this is updated regardless of whether the handler has reported any new inputs.
|
|
if (mouseSource == h)
|
|
reachedPreviousMouseSource = true;
|
|
}
|
|
|
|
mouseSourceDebounceTimeRemaining -= elapsed;
|
|
return inputs;
|
|
}
|
|
|
|
private readonly List<Drawable> inputQueue = new List<Drawable>();
|
|
|
|
private SlimReadOnlyListWrapper<Drawable> buildNonPositionalInputQueue()
|
|
{
|
|
inputQueue.Clear();
|
|
|
|
if (this is UserInputManager)
|
|
FrameStatistics.Increment(StatisticsCounterType.InputQueue);
|
|
|
|
var children = AliveInternalChildren;
|
|
|
|
for (int i = 0; i < children.Count; i++)
|
|
{
|
|
if (ShouldBeConsideredForInput(children[i]))
|
|
children[i].BuildNonPositionalInputQueue(inputQueue);
|
|
}
|
|
|
|
if (!unfocusIfNoLongerValid())
|
|
{
|
|
inputQueue.Remove(FocusedDrawable);
|
|
inputQueue.Add(FocusedDrawable);
|
|
}
|
|
|
|
// queues were created in back-to-front order.
|
|
// We want input to first reach front-most drawables, so the queues
|
|
// need to be reversed.
|
|
inputQueue.Reverse();
|
|
|
|
return inputQueue.AsSlimReadOnly();
|
|
}
|
|
|
|
private readonly List<Drawable> positionalInputQueue = new List<Drawable>();
|
|
|
|
private SlimReadOnlyListWrapper<Drawable> buildPositionalInputQueue(Vector2 screenSpacePos)
|
|
{
|
|
positionalInputQueue.Clear();
|
|
|
|
if (this is UserInputManager)
|
|
FrameStatistics.Increment(StatisticsCounterType.PositionalIQ);
|
|
|
|
var children = AliveInternalChildren;
|
|
|
|
for (int i = 0; i < children.Count; i++)
|
|
{
|
|
if (ShouldBeConsideredForInput(children[i]))
|
|
children[i].BuildPositionalInputQueue(screenSpacePos, positionalInputQueue);
|
|
}
|
|
|
|
positionalInputQueue.Reverse();
|
|
return positionalInputQueue.AsSlimReadOnly();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether this input manager is in a state it should handle hover events.
|
|
/// This could for instance be set to false when the window/target does not have input focus.
|
|
/// </summary>
|
|
public virtual bool HandleHoverEvents => true;
|
|
|
|
private void updateHoverEvents(InputState state)
|
|
{
|
|
Drawable lastHoverHandledDrawable = hoverHandledDrawable;
|
|
hoverHandledDrawable = null;
|
|
|
|
lastHoveredDrawables.Clear();
|
|
lastHoveredDrawables.AddRange(hoveredDrawables);
|
|
|
|
hoveredDrawables.Clear();
|
|
|
|
// New drawables shouldn't be hovered if the cursor isn't in the window
|
|
if (HandleHoverEvents)
|
|
{
|
|
// First, we need to construct hoveredDrawables for the current frame
|
|
foreach (Drawable d in PositionalInputQueue)
|
|
{
|
|
hoveredDrawables.Add(d);
|
|
lastHoveredDrawables.Remove(d);
|
|
|
|
// Don't need to re-hover those that are already hovered
|
|
if (d.IsHovered)
|
|
{
|
|
// Check if this drawable previously handled hover, and assume it would once more
|
|
if (d == lastHoverHandledDrawable)
|
|
{
|
|
hoverHandledDrawable = lastHoverHandledDrawable;
|
|
break;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
d.IsHovered = true;
|
|
|
|
if (d.TriggerEvent(new HoverEvent(state)))
|
|
{
|
|
hoverHandledDrawable = d;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Unhover all previously hovered drawables which are no longer hovered.
|
|
foreach (Drawable d in lastHoveredDrawables)
|
|
{
|
|
d.IsHovered = false;
|
|
d.TriggerEvent(new HoverLostEvent(state));
|
|
}
|
|
|
|
hoverEventsUpdated = true;
|
|
}
|
|
|
|
private bool isModifierKey(Key k) =>
|
|
k == Key.LControl || k == Key.RControl
|
|
|| k == Key.LAlt || k == Key.RAlt
|
|
|| k == Key.LShift || k == Key.RShift
|
|
|| k == Key.LWin || k == Key.RWin;
|
|
|
|
protected virtual void HandleKeyboardKeyStateChange(ButtonStateChangeEvent<Key> keyboardKeyStateChange)
|
|
{
|
|
var state = keyboardKeyStateChange.State;
|
|
var key = keyboardKeyStateChange.Button;
|
|
var kind = keyboardKeyStateChange.Kind;
|
|
|
|
GetButtonEventManagerFor(key).HandleButtonStateChange(state, kind);
|
|
|
|
if (kind == ButtonStateChangeKind.Pressed)
|
|
{
|
|
if (!isModifierKey(key))
|
|
{
|
|
keyboardRepeatKey = key;
|
|
keyboardRepeatTime = repeat_initial_delay;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (key == keyboardRepeatKey)
|
|
{
|
|
keyboardRepeatKey = null;
|
|
keyboardRepeatTime = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
protected virtual void HandleTouchStateChange(TouchStateChangeEvent e)
|
|
{
|
|
Debug.Assert(e.LastPosition != null || e.IsActive != null, $"A {nameof(TouchStateChangeEvent)} provided with no changes information.");
|
|
|
|
var manager = GetButtonEventManagerFor(e.Touch.Source);
|
|
|
|
if (e.LastPosition is Vector2 lastPosition)
|
|
manager.HandlePositionChange(e.State, lastPosition);
|
|
|
|
if (e.IsActive is bool active)
|
|
manager.HandleButtonStateChange(e.State, active ? ButtonStateChangeKind.Pressed : ButtonStateChangeKind.Released);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The number of touches which are currently active, causing a single cumulative "mouse down" state.
|
|
/// </summary>
|
|
private readonly HashSet<TouchSource> mouseMappedTouchesDown = new HashSet<TouchSource>();
|
|
|
|
private const double touch_right_click_delay = 750;
|
|
private const double touch_right_click_distance = 100;
|
|
|
|
/// <summary>
|
|
/// Invoked when a touch long-press gesture has scheduled for triggering after the specified delay.
|
|
/// </summary>
|
|
public event Action<Vector2, double> TouchLongPressBegan;
|
|
|
|
/// <summary>
|
|
/// Invoked when an ongoing touch long-press gesture has been cancelled.
|
|
/// </summary>
|
|
public event Action TouchLongPressCancelled;
|
|
|
|
[CanBeNull]
|
|
private ScheduledDelegate touchLongPressDelegate;
|
|
|
|
/// <summary>
|
|
/// The current position of the long-press touch, if one was begun.
|
|
/// </summary>
|
|
private Vector2? touchLongPressPosition;
|
|
|
|
/// <summary>
|
|
/// Whether the current touch state is eligible for performing a long-press gesture.
|
|
/// This is set to true once a single touch is pressed, and can be invalidated before the gesture is triggered.
|
|
/// </summary>
|
|
private bool validForLongPress;
|
|
|
|
/// <summary>
|
|
/// Handles latest activated touch state change event to produce mouse input from.
|
|
/// </summary>
|
|
/// <param name="e">The latest activated touch state change event.</param>
|
|
/// <returns>Whether mouse input has been performed accordingly.</returns>
|
|
protected virtual bool HandleMouseTouchStateChange(TouchStateChangeEvent e)
|
|
{
|
|
if (!MapMouseToLatestTouch)
|
|
return false;
|
|
|
|
if (e.IsActive == true || e.LastPosition != null)
|
|
{
|
|
new MousePositionAbsoluteInputFromTouch(e)
|
|
{
|
|
Position = e.Touch.Position
|
|
}.Apply(CurrentState, this);
|
|
}
|
|
|
|
if (e.IsActive != null)
|
|
{
|
|
if (e.IsActive == true)
|
|
mouseMappedTouchesDown.Add(e.Touch.Source);
|
|
else
|
|
mouseMappedTouchesDown.Remove(e.Touch.Source);
|
|
|
|
updateTouchMouseLeft(e);
|
|
}
|
|
|
|
updateTouchMouseRight(e);
|
|
return true;
|
|
}
|
|
|
|
private void updateTouchMouseLeft(TouchStateChangeEvent e)
|
|
{
|
|
if (mouseMappedTouchesDown.Count > 0)
|
|
new MouseButtonInputFromTouch(MouseButton.Left, true, e).Apply(CurrentState, this);
|
|
else
|
|
new MouseButtonInputFromTouch(MouseButton.Left, false, e).Apply(CurrentState, this);
|
|
}
|
|
|
|
private void updateTouchMouseRight(TouchStateChangeEvent e)
|
|
{
|
|
if (!AllowRightClickFromLongTouch)
|
|
return;
|
|
|
|
// if a touch was pressed/released in this event, reset gesture validity state.
|
|
if (e.IsActive != null)
|
|
validForLongPress = e.IsActive == true && mouseMappedTouchesDown.Count == 1;
|
|
|
|
bool gestureActive = touchLongPressDelegate != null;
|
|
|
|
if (gestureActive)
|
|
{
|
|
Debug.Assert(touchLongPressPosition != null);
|
|
|
|
// if a gesture was active and the user moved away from actuation point, invalidate gesture.
|
|
if (Vector2Extensions.Distance(touchLongPressPosition.Value, e.Touch.Position) > touch_right_click_distance)
|
|
validForLongPress = false;
|
|
|
|
if (!validForLongPress)
|
|
cancelTouchLongPress();
|
|
}
|
|
else
|
|
{
|
|
if (validForLongPress)
|
|
beginTouchLongPress(e);
|
|
}
|
|
}
|
|
|
|
private void beginTouchLongPress(TouchStateChangeEvent e)
|
|
{
|
|
touchLongPressPosition = e.Touch.Position;
|
|
|
|
TouchLongPressBegan?.Invoke(e.Touch.Position, touch_right_click_delay);
|
|
touchLongPressDelegate = Scheduler.AddDelayed(() =>
|
|
{
|
|
new MousePositionAbsoluteInputFromTouch(e) { Position = e.Touch.Position }.Apply(CurrentState, this);
|
|
new MouseButtonInputFromTouch(MouseButton.Right, true, e).Apply(CurrentState, this);
|
|
new MouseButtonInputFromTouch(MouseButton.Right, false, e).Apply(CurrentState, this);
|
|
|
|
// the touch actuated a long-press, releasing it should not perform a click.
|
|
GetButtonEventManagerFor(MouseButton.Left).BlockNextClick = true;
|
|
|
|
touchLongPressDelegate = null;
|
|
validForLongPress = false;
|
|
}, touch_right_click_delay);
|
|
}
|
|
|
|
private void cancelTouchLongPress()
|
|
{
|
|
Debug.Assert(touchLongPressDelegate != null);
|
|
|
|
touchLongPressPosition = null;
|
|
|
|
touchLongPressDelegate.Cancel();
|
|
touchLongPressDelegate = null;
|
|
|
|
TouchLongPressCancelled?.Invoke();
|
|
}
|
|
|
|
protected virtual void HandleTabletPenButtonStateChange(ButtonStateChangeEvent<TabletPenButton> tabletPenButtonStateChange)
|
|
=> GetButtonEventManagerFor(tabletPenButtonStateChange.Button).HandleButtonStateChange(tabletPenButtonStateChange.State, tabletPenButtonStateChange.Kind);
|
|
|
|
protected virtual void HandleTabletAuxiliaryButtonStateChange(ButtonStateChangeEvent<TabletAuxiliaryButton> tabletAuxiliaryButtonStateChange)
|
|
=> GetButtonEventManagerFor(tabletAuxiliaryButtonStateChange.Button).HandleButtonStateChange(tabletAuxiliaryButtonStateChange.State, tabletAuxiliaryButtonStateChange.Kind);
|
|
|
|
protected virtual void HandleJoystickButtonStateChange(ButtonStateChangeEvent<JoystickButton> joystickButtonStateChange)
|
|
=> GetButtonEventManagerFor(joystickButtonStateChange.Button).HandleButtonStateChange(joystickButtonStateChange.State, joystickButtonStateChange.Kind);
|
|
|
|
protected virtual void HandleMidiKeyStateChange(ButtonStateChangeEvent<MidiKey> midiKeyStateChange)
|
|
=> GetButtonEventManagerFor(midiKeyStateChange.Button).HandleButtonStateChange(midiKeyStateChange.State, midiKeyStateChange.Kind);
|
|
|
|
public virtual void HandleInputStateChange(InputStateChangeEvent inputStateChange)
|
|
{
|
|
switch (inputStateChange)
|
|
{
|
|
case MousePositionChangeEvent mousePositionChange:
|
|
HandleMousePositionChange(mousePositionChange);
|
|
return;
|
|
|
|
case MouseScrollChangeEvent mouseScrollChange:
|
|
HandleMouseScrollChange(mouseScrollChange);
|
|
return;
|
|
|
|
case ButtonStateChangeEvent<MouseButton> mouseButtonStateChange:
|
|
HandleMouseButtonStateChange(mouseButtonStateChange);
|
|
return;
|
|
|
|
case ButtonStateChangeEvent<Key> keyboardKeyStateChange:
|
|
HandleKeyboardKeyStateChange(keyboardKeyStateChange);
|
|
return;
|
|
|
|
case TouchStateChangeEvent touchChange:
|
|
var manager = GetButtonEventManagerFor(touchChange.Touch.Source);
|
|
|
|
bool touchWasHandled = manager.HeldDrawable != null;
|
|
|
|
HandleTouchStateChange(touchChange);
|
|
|
|
bool touchIsHandled = manager.HeldDrawable != null;
|
|
|
|
// Produce mouse input if no drawable in the input queue has handled this touch event.
|
|
// Done for compatibility with components that do not handle touch input directly.
|
|
if (!touchWasHandled && !touchIsHandled)
|
|
HandleMouseTouchStateChange(touchChange);
|
|
|
|
return;
|
|
|
|
case ButtonStateChangeEvent<TabletPenButton> tabletPenButtonStateChange:
|
|
HandleTabletPenButtonStateChange(tabletPenButtonStateChange);
|
|
return;
|
|
|
|
case ButtonStateChangeEvent<TabletAuxiliaryButton> tabletAuxiliaryButtonStateChange:
|
|
HandleTabletAuxiliaryButtonStateChange(tabletAuxiliaryButtonStateChange);
|
|
return;
|
|
|
|
case ButtonStateChangeEvent<JoystickButton> joystickButtonStateChange:
|
|
HandleJoystickButtonStateChange(joystickButtonStateChange);
|
|
return;
|
|
|
|
case JoystickAxisChangeEvent joystickAxisChangeEvent:
|
|
HandleJoystickAxisChange(joystickAxisChangeEvent);
|
|
return;
|
|
|
|
case ButtonStateChangeEvent<MidiKey> midiKeyStateChange:
|
|
HandleMidiKeyStateChange(midiKeyStateChange);
|
|
return;
|
|
}
|
|
}
|
|
|
|
protected virtual void HandleJoystickAxisChange(JoystickAxisChangeEvent e)
|
|
=> GetJoystickAxisEventManagerFor(e.Axis.Source).HandleAxisChange(e.State, e.Axis.Value, e.LastValue);
|
|
|
|
protected virtual void HandleMousePositionChange(MousePositionChangeEvent e)
|
|
{
|
|
var state = e.State;
|
|
var mouse = state.Mouse;
|
|
|
|
foreach (var h in InputHandlers)
|
|
{
|
|
if (h.Enabled.Value && h is INeedsMousePositionFeedback handler)
|
|
handler.FeedbackMousePositionChange(mouse.Position, h == mouseSource);
|
|
}
|
|
|
|
handleMouseMove(state, e.LastPosition);
|
|
|
|
foreach (var manager in mouseButtonEventManagers.Values)
|
|
manager.HandlePositionChange(state, e.LastPosition);
|
|
|
|
updateHoverEvents(state);
|
|
}
|
|
|
|
protected virtual void HandleMouseScrollChange(MouseScrollChangeEvent e)
|
|
{
|
|
handleScroll(e.State, e.LastScroll, e.IsPrecise);
|
|
}
|
|
|
|
protected virtual void HandleMouseButtonStateChange(ButtonStateChangeEvent<MouseButton> e)
|
|
{
|
|
if (mouseButtonEventManagers.TryGetValue(e.Button, out var manager))
|
|
manager.HandleButtonStateChange(e.State, e.Kind);
|
|
}
|
|
|
|
private bool handleMouseMove(InputState state, Vector2 lastPosition) => PropagateBlockableEvent(PositionalInputQueue, lastMouseMove = new MouseMoveEvent(state, lastPosition));
|
|
|
|
private bool handleScroll(InputState state, Vector2 lastScroll, bool isPrecise) =>
|
|
PropagateBlockableEvent(PositionalInputQueue, new ScrollEvent(state, state.Mouse.Scroll - lastScroll, isPrecise));
|
|
|
|
/// <summary>
|
|
/// Triggers events on drawables in <paramref name="drawables"/> until it is handled.
|
|
/// </summary>
|
|
/// <param name="drawables">The drawables in the queue.</param>
|
|
/// <param name="e">The event.</param>
|
|
/// <returns>Whether the event was handled.</returns>
|
|
protected virtual bool PropagateBlockableEvent(SlimReadOnlyListWrapper<Drawable> drawables, UIEvent e)
|
|
{
|
|
foreach (var d in drawables)
|
|
{
|
|
if (d.TriggerEvent(e))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unfocus the current focused drawable if it is no longer in a valid state.
|
|
/// </summary>
|
|
/// <returns>true if there is no longer a focus.</returns>
|
|
private bool unfocusIfNoLongerValid()
|
|
{
|
|
if (FocusedDrawable == null) return true;
|
|
|
|
if (isDrawableValidForFocus(FocusedDrawable))
|
|
return false;
|
|
|
|
Logger.Log($"Focus on \"{FocusedDrawable}\" no longer valid as a result of {nameof(unfocusIfNoLongerValid)}.", LoggingTarget.Runtime, LogLevel.Debug);
|
|
changeFocus(null);
|
|
return true;
|
|
}
|
|
|
|
private bool isDrawableValidForFocus(Drawable drawable)
|
|
{
|
|
bool valid = drawable.IsAlive && drawable.IsPresent && drawable.Parent != null;
|
|
|
|
if (valid)
|
|
{
|
|
//ensure we are visible
|
|
CompositeDrawable d = drawable.Parent;
|
|
|
|
while (d != null)
|
|
{
|
|
if (!d.IsPresent || !d.IsAlive)
|
|
{
|
|
valid = false;
|
|
break;
|
|
}
|
|
|
|
d = d.Parent;
|
|
}
|
|
}
|
|
|
|
return valid;
|
|
}
|
|
|
|
protected internal virtual void ChangeFocusFromClick(Drawable clickedDrawable)
|
|
{
|
|
Drawable focusTarget = null;
|
|
|
|
if (clickedDrawable != null)
|
|
{
|
|
focusTarget = clickedDrawable;
|
|
|
|
if (!focusTarget.AcceptsFocus)
|
|
{
|
|
// search upwards from the clicked drawable until we find something to handle focus.
|
|
Drawable previousFocused = FocusedDrawable;
|
|
|
|
while (focusTarget?.AcceptsFocus == false)
|
|
focusTarget = focusTarget.Parent;
|
|
|
|
if (focusTarget != null && previousFocused != null)
|
|
{
|
|
// we found a focusable target above us.
|
|
// now search upwards from previousFocused to check whether focusTarget is a common parent.
|
|
Drawable search = previousFocused;
|
|
while (search != null && search != focusTarget)
|
|
search = search.Parent;
|
|
|
|
if (focusTarget == search)
|
|
// we have a common parent, so let's keep focus on the previously focused target.
|
|
focusTarget = previousFocused;
|
|
}
|
|
}
|
|
}
|
|
|
|
changeFocus(focusTarget);
|
|
}
|
|
|
|
private void focusTopMostRequestingDrawable()
|
|
{
|
|
// todo: don't rebuild input queue every frame
|
|
foreach (var d in NonPositionalInputQueue)
|
|
{
|
|
if (d.RequestsFocus)
|
|
{
|
|
changeFocus(d);
|
|
return;
|
|
}
|
|
}
|
|
|
|
changeFocus(null);
|
|
}
|
|
|
|
private class MouseLeftButtonEventManager : MouseButtonEventManager
|
|
{
|
|
public MouseLeftButtonEventManager(MouseButton button)
|
|
: base(button)
|
|
{
|
|
}
|
|
|
|
public override bool EnableDrag => true;
|
|
|
|
public override bool EnableClick => true;
|
|
|
|
public override bool ChangeFocusOnClick => true;
|
|
}
|
|
|
|
private class MouseMinorButtonEventManager : MouseButtonEventManager
|
|
{
|
|
public MouseMinorButtonEventManager(MouseButton button)
|
|
: base(button)
|
|
{
|
|
}
|
|
|
|
public override bool EnableDrag => false;
|
|
|
|
public override bool EnableClick => false;
|
|
|
|
public override bool ChangeFocusOnClick => false;
|
|
}
|
|
}
|
|
}
|