// (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 } }