File: MS\Internal\Ink\LassoSelectionBehavior.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\PresentationFramework\PresentationFramework.csproj (PresentationFramework)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
//#define DEBUG_LASSO_FEEDBACK // DO NOT LEAVE ENABLED IN CHECKED IN CODE
//
// Description:
//      LassoSelectionBehavior
//
 
 
using MS.Internal.Controls;
using System.Collections;
using System.Windows.Input;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Ink;
using System.Windows.Media;
 
namespace MS.Internal.Ink
{
    /// <summary>
    /// Eraser Behavior
    /// </summary>
    internal sealed class LassoSelectionBehavior : StylusEditingBehavior
    {
        //-------------------------------------------------------------------------------
        //
        // Constructors
        //
        //-------------------------------------------------------------------------------
 
        #region Constructors
 
        internal LassoSelectionBehavior(EditingCoordinator editingCoordinator, InkCanvas inkCanvas)
            : base(editingCoordinator, inkCanvas)
        {
        }
 
        #endregion Constructors
 
        //-------------------------------------------------------------------------------
        //
        // Protected Methods
        //
        //-------------------------------------------------------------------------------
 
        #region Protected Methods
 
        /// <summary>
        /// Overrides SwitchToMode as the following expectations
        /// 31. From Select To InkAndGesture
        ///     Lasso is discarded. After mode change ink is being collected, gesture event fires. If its not a gesture, StrokeCollected event fires.
        /// 32. From Select To GestureOnly
        ///     Lasso is discarded. After mode change ink is being collected. On StylusUp gesture event fires. Stroke gets removed on StylusUp even if its not a gesture.
        /// 33. From Select To EraseByPoint
        ///     Lasso is discarded. PointErasing is performed after changing the mode.
        /// 34. From Select To EraseByStroke
        ///     Lasso is discarded. StrokeErasing is performed after changing the mode.
        /// 35. From Select To Ink
        ///     Ink collection starts when changing the mode.
        /// 36. From Select To None
        ///     Nothing gets selected.
        /// </summary>
        /// <param name="mode"></param>
        protected override void OnSwitchToMode(InkCanvasEditingMode mode)
        {
            Debug.Assert(EditingCoordinator.IsInMidStroke, "SwitchToMode should only be called in a mid-stroke");
 
            switch ( mode )
            {
                case InkCanvasEditingMode.Ink:
                case InkCanvasEditingMode.InkAndGesture:
                case InkCanvasEditingMode.GestureOnly:
                    {
                        // Discard the lasso
                        Commit(false);
 
                        // Change the mode. The dynamic renderer will be reset automatically.
                        EditingCoordinator.ChangeStylusEditingMode(this, mode);
                        break;
                    }
                case InkCanvasEditingMode.EraseByPoint:
                case InkCanvasEditingMode.EraseByStroke:
                    {
                        // Discard the lasso
                        Commit(false);
 
                        // Change the mode
                        EditingCoordinator.ChangeStylusEditingMode(this, mode);
 
                        break;
                    }
                case InkCanvasEditingMode.Select:
                    {
                        Debug.Assert(false, "Cannot switch from Select to Select in mid-stroke");
                        break;
                    }
                case InkCanvasEditingMode.None:
                    {
                        // Discard the lasso.
                        Commit(false);
 
                        // Change to the None mode
                        EditingCoordinator.ChangeStylusEditingMode(this, mode);
                        break;
                    }
                default:
                    Debug.Assert(false, "Unknown InkCanvasEditingMode!");
                    break;
            }
        }
 
        /// <summary>
        /// StylusInputBegin
        /// </summary>
        /// <param name="stylusPoints">stylusPoints</param>
        /// <param name="userInitiated">true if the source eventArgs.UserInitiated flag was set to true</param>
        protected override void StylusInputBegin(StylusPointCollection stylusPoints, bool userInitiated)
        {
            Debug.Assert(stylusPoints.Count != 0, "An empty stylusPoints has been passed in.");
 
            _disableLasso = false;
 
            bool startLasso = false;
 
            List<Point> points = new List<Point>();
            for ( int x = 0; x < stylusPoints.Count; x++ )
            {
                Point point = (Point)( stylusPoints[x] );
                if ( x == 0 )
                {
                    _startPoint = point;
                    points.Add(point);
                    continue;
                }
 
                if ( !startLasso )
                {
                    // If startLasso hasn't be flagged, we should check if the distance between two points is greater than 
                    // our tolerance. If so, we should flag startLasso.
                    Vector vector = point - _startPoint;
                    double distanceSquared = vector.LengthSquared;
 
                    if ( DoubleUtil.GreaterThan(distanceSquared, LassoHelper.MinDistanceSquared) )
                    {
                        points.Add(point);
                        startLasso = true;
                    }
                }
                else
                {
                    // The flag is set. We just add the point.
                    points.Add(point);
                }
            }
 
            // Start Lasso if it isn't a tap selection.
            if ( startLasso )
            {
                StartLasso(points);
            }
        }
 
        /// <summary>
        /// StylusInputContinue
        /// </summary>
        /// <param name="stylusPoints">stylusPoints</param>
        /// <param name="userInitiated">true if the source eventArgs.UserInitiated flag was set to true</param>
        protected override void StylusInputContinue(StylusPointCollection stylusPoints, bool userInitiated)
        {
            // Check whether Lasso has started.
            if ( _lassoHelper != null )
            {
                //
                // pump packets to the LassoHelper, it will convert them into an array of equidistant
                // lasso points, render those lasso point and return them to hit test against.
                //
                List<Point> points = new List<Point>();
                for ( int x = 0; x < stylusPoints.Count; x++ )
                {
                    points.Add((Point)stylusPoints[x]);
                }
                Point[] lassoPoints = _lassoHelper.AddPoints(points);
                if ( 0 != lassoPoints.Length )
                {
                    _incrementalLassoHitTester.AddPoints(lassoPoints);
                }
            }
            else if ( !_disableLasso )
            {
                // If Lasso hasn't start and been disabled, we should try to start it when it is needed.
                bool startLasso = false;
 
                List<Point> points = new List<Point>();
                for ( int x = 0; x < stylusPoints.Count; x++ )
                {
                    Point point = (Point)( stylusPoints[x] );
 
                    if ( !startLasso )
                    {
                        // If startLasso hasn't be flagged, we should check if the distance between two points is greater than 
                        // our tolerance. If so, we should flag startLasso.
                        Vector vector = point - _startPoint;
                        double distanceSquared = vector.LengthSquared;
 
                        if ( DoubleUtil.GreaterThan(distanceSquared, LassoHelper.MinDistanceSquared) )
                        {
                            points.Add(point);
                            startLasso = true;
                        }
                    }
                    else
                    {
                        // The flag is set. We just add the point.
                        points.Add(point);
                    }
                }
 
                // Start Lasso if it isn't a tap selection.
                if ( startLasso )
                {
                    StartLasso(points);
                }
            }
        }
 
 
        /// <summary>
        /// StylusInputEnd
        /// </summary>
        /// <param name="commit">commit</param>
        protected override void StylusInputEnd(bool commit)
        {
            // Initialize with empty selection
            StrokeCollection selectedStrokes = new StrokeCollection();
            List<UIElement> elementsToSelect = new List<UIElement>();
 
            if ( _lassoHelper != null )
            {
                // This is a lasso selection.
 
                //
                // end dynamic rendering
                //
                selectedStrokes = InkCanvas.EndDynamicSelection(_lassoHelper.Visual);
 
                //
                // hit test for elements
                //
                // NOTE: HitTestForElements uses the _lassoHelper so it must be alive at this point
                elementsToSelect = HitTestForElements();
 
                _incrementalLassoHitTester.SelectionChanged -= new LassoSelectionChangedEventHandler(OnSelectionChanged);
                _incrementalLassoHitTester.EndHitTesting();
                _incrementalLassoHitTester = null;
                _lassoHelper = null;
            }
            else
            {
                // This is a tap selection.
 
                // Now try the tap selection
                Stroke tappedStroke;
                UIElement tappedElement;
 
                TapSelectObject(_startPoint, out tappedStroke, out tappedElement);
 
                // If we have a pre-selected object, we should select it now.
                if ( tappedStroke != null )
                {
                    Debug.Assert(tappedElement == null);
                    selectedStrokes = new StrokeCollection();
                    selectedStrokes.Add(tappedStroke);
                }
                else if ( tappedElement != null )
                {
                    Debug.Assert(tappedStroke == null);
                    elementsToSelect.Add(tappedElement);
                }
            }
 
            SelfDeactivate();
 
            if ( commit )
            {
                InkCanvas.ChangeInkCanvasSelection(selectedStrokes, elementsToSelect.ToArray());
            }
        }
 
        /// <summary>
        /// OnCommitWithoutStylusInput
        /// </summary>
        /// <param name="commit"></param>
        protected override void OnCommitWithoutStylusInput(bool commit)
        {
            // We only deactivate LSB in this case.
            SelfDeactivate();
        }
 
        /// <summary>
        /// Get the current cursor for this editing mode
        /// </summary>
        /// <returns></returns>
        protected override Cursor GetCurrentCursor()
        {
            // By default return cross cursor.
            return Cursors.Cross;
        }
 
        #endregion Protected Methods
 
        //-------------------------------------------------------------------------------
        //
        // Private Methods
        //
        //-------------------------------------------------------------------------------
 
        #region Private Methods
 
        /// <summary>
        /// Private event handler that updates which strokes are actually selected
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnSelectionChanged(object sender, LassoSelectionChangedEventArgs e)
        {
            this.InkCanvas.UpdateDynamicSelection(e.SelectedStrokes, e.DeselectedStrokes);
        }
 
 
        /// <summary>
        /// Private helper that will hit test for elements
        /// </summary>
        private List<UIElement> HitTestForElements()
        {
            List<UIElement> elementsToSelect = new List<UIElement>();
 
            if ( this.InkCanvas.Children.Count == 0 )
            {
                return elementsToSelect;
            }
 
            for (int x = 0; x < this.InkCanvas.Children.Count; x++)
            {
                UIElement uiElement = this.InkCanvas.Children[x];
                HitTestElement(InkCanvas.InnerCanvas, uiElement, elementsToSelect);
            }
 
            return elementsToSelect;
        }
 
        /// <summary>
        /// Private helper that will turn an element in any nesting level into a stroke
        /// in the InkCanvas's coordinate space.  This method calls itself recursively
        /// </summary>
        private void HitTestElement(InkCanvasInnerCanvas parent, UIElement uiElement, List<UIElement> elementsToSelect)
        {
            ElementCornerPoints elementPoints = LassoSelectionBehavior.GetTransformedElementCornerPoints(parent, uiElement);
            if (elementPoints.Set != false)
            {
                Point[] points = GeneratePointGrid(elementPoints);
 
                //
                // perform hit testing against our lasso
                //
                System.Diagnostics.Debug.Assert(null != _lassoHelper);
                if (_lassoHelper.ArePointsInLasso(points, _percentIntersectForElements))
                {
                    elementsToSelect.Add(uiElement);
                }
            }
            //
            // we used to recurse into the childrens children.  That is no longer necessary
            //
        }
 
        /// <summary>
        /// Private helper that takes an element and transforms it's 4 points
        /// into the InkCanvas's space
        /// </summary>
        private static ElementCornerPoints GetTransformedElementCornerPoints(InkCanvasInnerCanvas canvas, UIElement childElement)
        {
            Debug.Assert(canvas != null);
            Debug.Assert(childElement != null);
 
            Debug.Assert(canvas.CheckAccess());
 
            ElementCornerPoints elementPoints = new ElementCornerPoints();
            elementPoints.Set = false;
 
            if (childElement.Visibility != Visibility.Visible)
            {
                //
                // this little one's not worth it...
                //
                return elementPoints;
            }
 
            //
            // get the transform from us to our parent InkCavas
            //
            GeneralTransform parentTransform = childElement.TransformToAncestor(canvas);            
            
            // REVIEW: any of the methods below may not actually perform the transformation
            // Do we need to do anything special in that scenario?
            parentTransform.TryTransform(new Point(0, 0), out elementPoints.UpperLeft);
            parentTransform.TryTransform(new Point(childElement.RenderSize.Width, 0), out elementPoints.UpperRight);
            parentTransform.TryTransform(new Point(0, childElement.RenderSize.Height), out elementPoints.LowerLeft);
            parentTransform.TryTransform(new Point(childElement.RenderSize.Width, childElement.RenderSize.Height), out elementPoints.LowerRight);
 
            elementPoints.Set = true;
            return elementPoints;
        }
 
        /// <summary>
        /// Private helper that will generate a grid of points 5 px apart given the elements bounding points
        /// this works with any affline transformed points
        /// </summary>
        private Point[] GeneratePointGrid(ElementCornerPoints elementPoints)
        {
            if (!elementPoints.Set)
            {
                return new Point[]{};
            }
            ArrayList pointArray = new ArrayList();
 
            UpdatePointDistances(elementPoints);
 
            //
            // add our original points
            //
            pointArray.Add(elementPoints.UpperLeft);
            pointArray.Add(elementPoints.UpperRight);
            FillInPoints(pointArray, elementPoints.UpperLeft, elementPoints.UpperRight);
 
            pointArray.Add(elementPoints.LowerLeft);
            pointArray.Add(elementPoints.LowerRight);
            FillInPoints(pointArray, elementPoints.LowerLeft, elementPoints.LowerRight);
 
            FillInGrid( pointArray,
                        elementPoints.UpperLeft,
                        elementPoints.UpperRight,
                        elementPoints.LowerRight,
                        elementPoints.LowerLeft);
 
            Point[] retPointArray = new Point[pointArray.Count];
            pointArray.CopyTo(retPointArray);
            return retPointArray;
        }
 
        /// <summary>
        /// Private helper that fills in the points between two points by calling itself
        /// recursively in a divide and conquer fashion
        /// </summary>
        private void FillInPoints(ArrayList pointArray, Point point1, Point point2)
        {
            // this algorithm improves perf by 20%
            if(!PointsAreCloseEnough(point1, point2))
            {
                Point midPoint = LassoSelectionBehavior.GeneratePointBetweenPoints(point1, point2);
                pointArray.Add(midPoint);
 
                if(!PointsAreCloseEnough(point1, midPoint))
                {
                    FillInPoints(pointArray, point1, midPoint);
                }
 
                //sort the right
                if(!PointsAreCloseEnough(midPoint, point2))
                {
                    FillInPoints(pointArray, midPoint, point2);
                }
            }
        }
 
        /// <summary>
        /// Private helper that fills in the points between four points by calling itself
        /// recursively in a divide and conquer fashion
        /// </summary>
        private void FillInGrid(ArrayList pointArray,
                                Point upperLeft,
                                Point upperRight,
                                Point lowerRight,
                                Point lowerLeft)
        {
            // this algorithm improves perf by 20%
            if(!PointsAreCloseEnough(upperLeft, lowerLeft))
            {
                Point midPointLeft = LassoSelectionBehavior.GeneratePointBetweenPoints(upperLeft, lowerLeft);
                Point midPointRight = LassoSelectionBehavior.GeneratePointBetweenPoints(upperRight, lowerRight);
                pointArray.Add(midPointLeft);
                pointArray.Add(midPointRight);
                FillInPoints(pointArray, midPointLeft, midPointRight);
 
                if(!PointsAreCloseEnough(upperLeft, midPointLeft))
                {
                    FillInGrid(pointArray, upperLeft, upperRight, midPointRight, midPointLeft);
                }
 
                //sort the right
                if(!PointsAreCloseEnough(midPointLeft, lowerLeft))
                {
                    FillInGrid(pointArray, midPointLeft, midPointRight, lowerRight, lowerLeft);
                }
            }
        }
 
        /// <summary>
        /// Private helper that will generate a new point between two points
        /// </summary>
        private static Point GeneratePointBetweenPoints(Point point1, Point point2)
        {
            //
            // compute the new point in the middle of the previous two
            //
            double maxX = point1.X > point2.X ? point1.X : point2.X;
            double minX = point1.X < point2.X ? point1.X : point2.X;
            double maxY = point1.Y > point2.Y ? point1.Y : point2.Y;
            double minY = point1.Y < point2.Y ? point1.Y : point2.Y;
 
            return new Point( (minX + ((maxX - minX) * 0.5f)),
                                (minY + ((maxY - minY) * 0.5f)));
        }
 
        /// <summary>
        /// Private helper used to determine if we're close enough between two points
        /// </summary>
        private bool PointsAreCloseEnough(Point point1, Point point2)
        {
            double x = point1.X - point2.X;
            double y = point1.Y - point2.Y;
            if ((x < _xDiff && x > -_xDiff) && (y < _yDiff && y > -_yDiff))
            {
                return true;
            }
            return false;
        }
 
        /// <summary>
        /// Used to calc the diff between points on the x and y axis
        /// </summary>
        private void UpdatePointDistances(ElementCornerPoints elementPoints)
        {
            //
            // calc the x and y diffs
            //
            double width = elementPoints.UpperLeft.X - elementPoints.UpperRight.X;
            if (width < 0)
            {
                width = -width;
            }
 
            double height = elementPoints.UpperLeft.Y - elementPoints.LowerLeft.Y;
            if (height < 0)
            {
                height = -height;
            }
 
            _xDiff = width * 0.25f;
            if (_xDiff > _maxThreshold)
            {
                _xDiff = _maxThreshold;
            }
            else if (_xDiff < _minThreshold)
            {
                _xDiff = _minThreshold;
            }
 
            _yDiff = height * 0.25f;
            if (_yDiff > _maxThreshold)
            {
                _yDiff = _maxThreshold;
            }
            else if (_yDiff < _minThreshold)
            {
                _yDiff = _minThreshold;
            }
        }
 
        /// <summary>
        /// StartLasso
        /// </summary>
        /// <param name="points"></param>
        private void StartLasso(List<Point> points)
        {
            Debug.Assert(!_disableLasso && _lassoHelper == null, "StartLasso is called unexpectedly.");
 
            if ( InkCanvas.ClearSelectionRaiseSelectionChanging() // If user cancels clearing the selection, we shouldn't initiate Lasso.
                // 
                // If the active editng mode is no longer as Select, we shouldn't activate LassoSelectionBehavior.
                // Note the order really matters here. This checking has to be done 
                // after ClearSelectionRaiseSelectionChanging is invoked.
                && EditingCoordinator.ActiveEditingMode == InkCanvasEditingMode.Select )
            {
                //
                // obtain a dynamic hit-tester for selecting with lasso
                //
                _incrementalLassoHitTester =
                                    this.InkCanvas.Strokes.GetIncrementalLassoHitTester(_percentIntersectForInk);
                //
                // add event handler
                //
                _incrementalLassoHitTester.SelectionChanged += new LassoSelectionChangedEventHandler(OnSelectionChanged);
 
                //
                // start dynamic rendering
                //
 
                _lassoHelper = new LassoHelper();
                InkCanvas.BeginDynamicSelection(_lassoHelper.Visual);
 
                Point[] lassoPoints = _lassoHelper.AddPoints(points);
                if ( 0 != lassoPoints.Length )
                {
                    _incrementalLassoHitTester.AddPoints(lassoPoints);
                }
            }
            else
            {
                // If we fail on clearing the selection or switching to Select mode, we should just disable lasso.
                _disableLasso = true;
            }
        }
 
        /// <summary>
        /// Pre-Select the object which user taps on.
        /// </summary>
        /// <param name="point"></param>
        /// <param name="tappedStroke"></param>
        /// <param name="tappedElement"></param>
        private void TapSelectObject(Point point, out Stroke tappedStroke, out UIElement tappedElement)
        {
            tappedStroke = null;
            tappedElement = null;
 
            StrokeCollection hitTestStrokes = InkCanvas.Strokes.HitTest(point, 5.0d);
            if ( hitTestStrokes.Count > 0 )
            {
                tappedStroke = hitTestStrokes[hitTestStrokes.Count - 1];
            }
            else
            {
                GeneralTransform transformToInnerCanvas = InkCanvas.TransformToVisual(InkCanvas.InnerCanvas);
                Point pointOnInnerCanvas = transformToInnerCanvas.Transform(point);
 
                // Try to find out whether we have a pre-select object.
                tappedElement = InkCanvas.InnerCanvas.HitTestOnElements(pointOnInnerCanvas);
            }
}
 
        #endregion Private Methods
 
        //-------------------------------------------------------------------------------
        //
        // Private Fields
        //
        //-------------------------------------------------------------------------------
 
        #region Private Fields
 
        private Point                       _startPoint;
        private bool                        _disableLasso;
 
        private LassoHelper                 _lassoHelper = null;
        private IncrementalLassoHitTester   _incrementalLassoHitTester;
        private double                  _xDiff;
        private double                  _yDiff;
        private const double            _maxThreshold = 50f;
        private const double            _minThreshold = 15f;
        private const int               _percentIntersectForInk = 80;
        private const int               _percentIntersectForElements = 60;
 
 
#if DEBUG_LASSO_FEEDBACK
        private ContainerVisual _tempVisual;
#endif
        /// <summary>
        /// Private struct
        /// </summary>
        private struct ElementCornerPoints
        {
            internal Point UpperLeft;
            internal Point UpperRight;
            internal Point LowerRight;
            internal Point LowerLeft;
            internal bool Set;
        }
 
        #endregion Private Fields
    }
}