// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993] for details.
// All other rights reserved.
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace System.Windows.Controls
{
///
/// The InteractionHelper provides controls with support for all of the
/// common interactions like mouse movement, mouse clicks, key presses,
/// etc., and also incorporates proper event semantics when the control is
/// disabled.
///
internal sealed partial class InteractionHelper
{
// TODO: Consult with user experience experts to validate the double
// click distance and time thresholds.
///
/// The threshold used to determine whether two clicks are temporally
/// local and considered a double click (or triple, quadruple, etc.).
/// 500 milliseconds is the default double click value on Windows.
/// This value would ideally be pulled form the system settings.
///
private const double SequentialClickThresholdInMilliseconds = 500.0;
///
/// The threshold used to determine whether two clicks are spatially
/// local and considered a double click (or triple, quadruple, etc.)
/// in pixels squared. We use pixels squared so that we can compare to
/// the distance delta without taking a square root.
///
private const double SequentialClickThresholdInPixelsSquared = 3.0 * 3.0;
///
/// Gets the control the InteractionHelper is targeting.
///
public Control Control { get; private set; }
///
/// Gets a value indicating whether the control has focus.
///
public bool IsFocused { get; private set; }
///
/// Gets a value indicating whether the mouse is over the control.
///
public bool IsMouseOver { get; private set; }
///
/// Gets a value indicating whether the read-only property is set.
///
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Linked file.")]
public bool IsReadOnly { get; private set; }
///
/// Gets a value indicating whether the mouse button is pressed down
/// over the control.
///
public bool IsPressed { get; private set; }
///
/// Gets or sets the last time the control was clicked.
///
///
/// The value is stored as Utc time because it is slightly more
/// performant than converting to local time.
///
private DateTime LastClickTime { get; set; }
///
/// Gets or sets the mouse position of the last click.
///
/// The value is relative to the control.
private Point LastClickPosition { get; set; }
///
/// Gets the number of times the control was clicked.
///
public int ClickCount { get; private set; }
///
/// Reference used to call UpdateVisualState on the base class.
///
private IUpdateVisualState _updateVisualState;
///
/// Initializes a new instance of the InteractionHelper class.
///
/// Control receiving interaction.
public InteractionHelper(Control control)
{
Debug.Assert(control != null, "control should not be null!");
Control = control;
_updateVisualState = control as IUpdateVisualState;
// Wire up the event handlers for events without a virtual override
control.Loaded += OnLoaded;
control.IsEnabledChanged += OnIsEnabledChanged;
}
#region UpdateVisualState
///
/// Update the visual state of the control.
///
///
/// A value indicating whether to automatically generate transitions to
/// the new state, or instantly transition to the new state.
///
///
/// UpdateVisualState works differently than the rest of the injected
/// functionality. Most of the other events are overridden by the
/// calling class which calls Allow, does what it wants, and then calls
/// Base. UpdateVisualState is the opposite because a number of the
/// methods in InteractionHelper need to trigger it in the calling
/// class. We do this using the IUpdateVisualState internal interface.
///
private void UpdateVisualState(bool useTransitions)
{
if (_updateVisualState != null)
{
_updateVisualState.UpdateVisualState(useTransitions);
}
}
///
/// Update the visual state of the control.
///
///
/// A value indicating whether to automatically generate transitions to
/// the new state, or instantly transition to the new state.
///
public void UpdateVisualStateBase(bool useTransitions)
{
// Handle the Common states
if (!Control.IsEnabled)
{
VisualStates.GoToState(Control, useTransitions, VisualStates.StateDisabled, VisualStates.StateNormal);
}
else if (IsReadOnly)
{
VisualStates.GoToState(Control, useTransitions, VisualStates.StateReadOnly, VisualStates.StateNormal);
}
else if (IsPressed)
{
VisualStates.GoToState(Control, useTransitions, VisualStates.StatePressed, VisualStates.StateMouseOver, VisualStates.StateNormal);
}
else if (IsMouseOver)
{
VisualStates.GoToState(Control, useTransitions, VisualStates.StateMouseOver, VisualStates.StateNormal);
}
else
{
VisualStates.GoToState(Control, useTransitions, VisualStates.StateNormal);
}
// Handle the Focused states
if (IsFocused)
{
VisualStates.GoToState(Control, useTransitions, VisualStates.StateFocused, VisualStates.StateUnfocused);
}
else
{
VisualStates.GoToState(Control, useTransitions, VisualStates.StateUnfocused);
}
}
#endregion UpdateVisualState
///
/// Handle the control's Loaded event.
///
/// The control.
/// Event arguments.
private void OnLoaded(object sender, RoutedEventArgs e)
{
UpdateVisualState(false);
}
///
/// Handle changes to the control's IsEnabled property.
///
/// The control.
/// Event arguments.
private void OnIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
{
bool enabled = (bool) e.NewValue;
if (!enabled)
{
IsPressed = false;
IsMouseOver = false;
IsFocused = false;
}
UpdateVisualState(true);
}
///
/// Handles changes to the control's IsReadOnly property.
///
/// The value of the property.
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Linked file.")]
public void OnIsReadOnlyChanged(bool value)
{
IsReadOnly = value;
if (!value)
{
IsPressed = false;
IsMouseOver = false;
IsFocused = false;
}
UpdateVisualState(true);
}
///
/// Update the visual state of the control when its template is changed.
///
public void OnApplyTemplateBase()
{
UpdateVisualState(false);
}
#region GotFocus
///
/// Check if the control's GotFocus event should be handled.
///
/// Event arguments.
///
/// A value indicating whether the event should be handled.
///
public bool AllowGotFocus(RoutedEventArgs e)
{
if (e == null)
{
throw new ArgumentNullException("e");
}
bool enabled = Control.IsEnabled;
if (enabled)
{
IsFocused = true;
}
return enabled;
}
///
/// Base implementation of the virtual GotFocus event handler.
///
public void OnGotFocusBase()
{
UpdateVisualState(true);
}
#endregion GotFocus
#region LostFocus
///
/// Check if the control's LostFocus event should be handled.
///
/// Event arguments.
///
/// A value indicating whether the event should be handled.
///
public bool AllowLostFocus(RoutedEventArgs e)
{
if (e == null)
{
throw new ArgumentNullException("e");
}
bool enabled = Control.IsEnabled;
if (enabled)
{
IsFocused = false;
}
return enabled;
}
///
/// Base implementation of the virtual LostFocus event handler.
///
public void OnLostFocusBase()
{
IsPressed = false;
UpdateVisualState(true);
}
#endregion LostFocus
#region MouseEnter
///
/// Check if the control's MouseEnter event should be handled.
///
/// Event arguments.
///
/// A value indicating whether the event should be handled.
///
public bool AllowMouseEnter(MouseEventArgs e)
{
if (e == null)
{
throw new ArgumentNullException("e");
}
bool enabled = Control.IsEnabled;
if (enabled)
{
IsMouseOver = true;
}
return enabled;
}
///
/// Base implementation of the virtual MouseEnter event handler.
///
public void OnMouseEnterBase()
{
UpdateVisualState(true);
}
#endregion MouseEnter
#region MouseLeave
///
/// Check if the control's MouseLeave event should be handled.
///
/// Event arguments.
///
/// A value indicating whether the event should be handled.
///
public bool AllowMouseLeave(MouseEventArgs e)
{
if (e == null)
{
throw new ArgumentNullException("e");
}
bool enabled = Control.IsEnabled;
if (enabled)
{
IsMouseOver = false;
}
return enabled;
}
///
/// Base implementation of the virtual MouseLeave event handler.
///
public void OnMouseLeaveBase()
{
UpdateVisualState(true);
}
#endregion MouseLeave
#region MouseLeftButtonDown
///
/// Check if the control's MouseLeftButtonDown event should be handled.
///
/// Event arguments.
///
/// A value indicating whether the event should be handled.
///
public bool AllowMouseLeftButtonDown(MouseButtonEventArgs e)
{
if (e == null)
{
throw new ArgumentNullException("e");
}
bool enabled = Control.IsEnabled;
if (enabled)
{
// Get the current position and time
DateTime now = DateTime.UtcNow;
Point position = e.GetPosition(Control);
// Compute the deltas from the last click
double timeDelta = (now - LastClickTime).TotalMilliseconds;
Point lastPosition = LastClickPosition;
double dx = position.X - lastPosition.X;
double dy = position.Y - lastPosition.Y;
double distance = dx * dx + dy * dy;
// Check if the values fall under the sequential click temporal
// and spatial thresholds
if (timeDelta < SequentialClickThresholdInMilliseconds &&
distance < SequentialClickThresholdInPixelsSquared)
{
// TODO: Does each click have to be within the single time
// threshold on WPF?
ClickCount++;
}
else
{
ClickCount = 1;
}
// Set the new position and time
LastClickTime = now;
LastClickPosition = position;
// Raise the event
IsPressed = true;
}
else
{
ClickCount = 1;
}
return enabled;
}
///
/// Base implementation of the virtual MouseLeftButtonDown event
/// handler.
///
public void OnMouseLeftButtonDownBase()
{
UpdateVisualState(true);
}
#endregion MouseLeftButtonDown
#region MouseLeftButtonUp
///
/// Check if the control's MouseLeftButtonUp event should be handled.
///
/// Event arguments.
///
/// A value indicating whether the event should be handled.
///
public bool AllowMouseLeftButtonUp(MouseButtonEventArgs e)
{
if (e == null)
{
throw new ArgumentNullException("e");
}
bool enabled = Control.IsEnabled;
if (enabled)
{
IsPressed = false;
}
return enabled;
}
///
/// Base implementation of the virtual MouseLeftButtonUp event handler.
///
public void OnMouseLeftButtonUpBase()
{
UpdateVisualState(true);
}
#endregion MouseLeftButtonUp
#region KeyDown
///
/// Check if the control's KeyDown event should be handled.
///
/// Event arguments.
///
/// A value indicating whether the event should be handled.
///
public bool AllowKeyDown(KeyEventArgs e)
{
if (e == null)
{
throw new ArgumentNullException("e");
}
return Control.IsEnabled;
}
#endregion KeyDown
#region KeyUp
///
/// Check if the control's KeyUp event should be handled.
///
/// Event arguments.
///
/// A value indicating whether the event should be handled.
///
public bool AllowKeyUp(KeyEventArgs e)
{
if (e == null)
{
throw new ArgumentNullException("e");
}
return Control.IsEnabled;
}
#endregion KeyUp
}
}