// Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; using System.Linq; using System.Reactive.Linq; using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.Platform; using Avalonia.VisualTree; namespace Avalonia.Input { /// /// Represents a mouse device. /// public class MouseDevice : IMouseDevice { private int _clickCount; private Rect _lastClickRect; private ulong _lastClickTime; private readonly Pointer _pointer; public MouseDevice(Pointer pointer = null) { _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); } /// /// Gets the control that is currently capturing by the mouse, if any. /// /// /// When an element captures the mouse, it receives mouse input whether the cursor is /// within the control's bounds or not. To set the mouse capture, call the /// method. /// [Obsolete("Use IPointer instead")] public IInputElement Captured => _pointer.Captured; /// /// Gets the mouse position, in screen coordinates. /// public PixelPoint Position { get; protected set; } /// /// Captures mouse input to the specified control. /// /// The control. /// /// When an element captures the mouse, it receives mouse input whether the cursor is /// within the control's bounds or not. The current mouse capture control is exposed /// by the property. /// public void Capture(IInputElement control) { _pointer.Capture(control); } /// /// Gets the mouse position relative to a control. /// /// The control. /// The mouse position in the control's coordinates. public Point GetPosition(IVisual relativeTo) { Contract.Requires(relativeTo != null); if (relativeTo.VisualRoot == null) { throw new InvalidOperationException("Control is not attached to visual tree."); } var rootPoint = relativeTo.VisualRoot.PointToClient(Position); var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); return rootPoint * transform.Value; } public void ProcessRawEvent(RawInputEventArgs e) { if (!e.Handled && e is RawPointerEventArgs margs) ProcessRawEvent(margs); } public void SceneInvalidated(IInputRoot root, Rect rect) { var clientPoint = root.PointToClient(Position); if (rect.Contains(clientPoint)) { if (_pointer.Captured == null) { SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, PointerPointProperties.None, KeyModifiers.None); } else { SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, PointerPointProperties.None, KeyModifiers.None); } } } int ButtonCount(PointerPointProperties props) { var rv = 0; if (props.IsLeftButtonPressed) rv++; if (props.IsMiddleButtonPressed) rv++; if (props.IsRightButtonPressed) rv++; return rv; } private void ProcessRawEvent(RawPointerEventArgs e) { Contract.Requires(e != null); var mouse = (IMouseDevice)e.Device; Position = e.Root.PointToScreen(e.Position); var props = CreateProperties(e); var keyModifiers = KeyModifiersUtils.ConvertToKey(e.InputModifiers); switch (e.Type) { case RawPointerEventType.LeaveWindow: LeaveWindow(mouse, e.Timestamp, e.Root, props, keyModifiers); break; case RawPointerEventType.LeftButtonDown: case RawPointerEventType.RightButtonDown: case RawPointerEventType.MiddleButtonDown: if (ButtonCount(props) > 1) e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); else e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); break; case RawPointerEventType.LeftButtonUp: case RawPointerEventType.RightButtonUp: case RawPointerEventType.MiddleButtonUp: if (ButtonCount(props) != 0) e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); else e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); break; case RawPointerEventType.Move: e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); break; case RawPointerEventType.Wheel: e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers); break; } } private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, KeyModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); ClearPointerOver(this, timestamp, root, properties, inputModifiers); } PointerPointProperties CreateProperties(RawPointerEventArgs args) { var kind = PointerUpdateKind.Other; if (args.Type == RawPointerEventType.LeftButtonDown) kind = PointerUpdateKind.LeftButtonPressed; if (args.Type == RawPointerEventType.MiddleButtonDown) kind = PointerUpdateKind.MiddleButtonPressed; if (args.Type == RawPointerEventType.RightButtonDown) kind = PointerUpdateKind.RightButtonPressed; if (args.Type == RawPointerEventType.LeftButtonUp) kind = PointerUpdateKind.LeftButtonReleased; if (args.Type == RawPointerEventType.MiddleButtonUp) kind = PointerUpdateKind.MiddleButtonReleased; if (args.Type == RawPointerEventType.RightButtonUp) kind = PointerUpdateKind.RightButtonReleased; return new PointerPointProperties(args.InputModifiers, kind); } private MouseButton _lastMouseDownButton; private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, PointerPointProperties properties, KeyModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); var hit = HitTest(root, p); if (hit != null) { _pointer.Capture(hit); var source = GetSource(hit); if (source != null) { var settings = AvaloniaLocator.Current.GetService(); var doubleClickTime = settings.DoubleClickTime.TotalMilliseconds; if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) { _clickCount = 0; } ++_clickCount; _lastClickTime = timestamp; _lastClickRect = new Rect(p, new Size()) .Inflate(new Thickness(settings.DoubleClickSize.Width / 2, settings.DoubleClickSize.Height / 2)); _lastMouseDownButton = properties.GetObsoleteMouseButton(); var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount); source.RaiseEvent(e); return e.Handled; } } return false; } private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, KeyModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); IInputElement source; if (_pointer.Captured == null) { source = SetPointerOver(this, timestamp, root, p, properties, inputModifiers); } else { SetPointerOver(this, timestamp, root, _pointer.Captured, properties, inputModifiers); source = _pointer.Captured; } var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, p, timestamp, properties, inputModifiers); source?.RaiseEvent(e); return e.Handled; } private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, KeyModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); var hit = HitTest(root, p); if (hit != null) { var source = GetSource(hit); var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers); source?.RaiseEvent(e); _pointer.Capture(null); return e.Handled; } return false; } private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); var hit = HitTest(root, p); if (hit != null) { var source = GetSource(hit); var e = new PointerWheelEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); source?.RaiseEvent(e); return e.Handled; } return false; } private IInteractive GetSource(IVisual hit) { Contract.Requires(hit != null); return _pointer.Captured ?? (hit as IInteractive) ?? hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); } private IInputElement HitTest(IInputElement root, Point p) { Contract.Requires(root != null); return _pointer.Captured ?? root.InputHitTest(p); } PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive source, PointerPointProperties properties, KeyModifiers inputModifiers) { return new PointerEventArgs(ev, source, _pointer, null, default, timestamp, properties, inputModifiers); } private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, KeyModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); var element = root.PointerOverElement; var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers); if (element!=null && !element.IsAttachedToVisualTree) { // element has been removed from visual tree so do top down cleanup if (root.IsPointerOver) ClearChildrenPointerOver(e, root,true); } while (element != null) { e.Source = element; e.Handled = false; element.RaiseEvent(e); element = (IInputElement)element.VisualParent; } root.PointerOverElement = null; } private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element,bool clearRoot) { foreach (IInputElement el in element.VisualChildren) { if (el.IsPointerOver) { ClearChildrenPointerOver(e, el, true); break; } } if(clearRoot) { e.Source = element; e.Handled = false; element.RaiseEvent(e); } } private IInputElement SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, KeyModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); var element = root.InputHitTest(p); if (element != root.PointerOverElement) { if (element != null) { SetPointerOver(device, timestamp, root, element, properties, inputModifiers); } else { ClearPointerOver(device, timestamp, root, properties, inputModifiers); } } return element; } private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, PointerPointProperties properties, KeyModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); Contract.Requires(element != null); IInputElement branch = null; var el = element; while (el != null) { if (el.IsPointerOver) { branch = el; break; } el = (IInputElement)el.VisualParent; } el = root.PointerOverElement; var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, properties, inputModifiers); if (el!=null && branch!=null && !el.IsAttachedToVisualTree) { ClearChildrenPointerOver(e,branch,false); } while (el != null && el != branch) { e.Source = el; e.Handled = false; el.RaiseEvent(e); el = (IInputElement)el.VisualParent; } el = root.PointerOverElement = element; e.RoutedEvent = InputElement.PointerEnterEvent; while (el != null && el != branch) { e.Source = el; e.Handled = false; el.RaiseEvent(e); el = (IInputElement)el.VisualParent; } } } }