File: MS\Internal\Annotations\Anchoring\TextSelectionProcessor.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.
 
//
//
// Description:
//     TextSelectionProcessor uses TextAnchors to represent portions
//     of text that are anchors.  It produces locator parts that
//     represent these TextAnchors and can generate TextAnchors from
//     the locator parts.
//     Spec: Anchoring Namespace Spec.doc
//
 
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Windows;
using System.Windows.Annotations;
using System.Windows.Annotations.Storage;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Media;
using System.Xml;
using MS.Utility;
 
using MS.Internal.Documents;
 
namespace MS.Internal.Annotations.Anchoring
{
    /// <summary>
    ///     TextSelectionProcessor uses TextAnchors to represent portions
    ///     of text that are anchors.  It produces locator parts that
    ///     represent these TextAnchors and can generate TextAnchors from
    ///     the locator parts.
    /// </summary>
    internal sealed class TextSelectionProcessor : SelectionProcessor
    {
        //------------------------------------------------------
        //
        //  Constructors
        //
        //------------------------------------------------------
 
        #region Constructors
 
        /// <summary>
        ///     Creates an instance of TextSelectionProcessor.
        /// </summary>
        public TextSelectionProcessor()
        {
        }
 
        #endregion Constructors
 
        //------------------------------------------------------
        //
        //  Public Methods
        //
        //------------------------------------------------------
 
        #region Public Methods
 
        /// <summary>
        ///     Merges the two anchors into one, if possible.
        /// </summary>
        /// <param name="anchor1">anchor to merge </param>
        /// <param name="anchor2">other anchor to merge </param>
        /// <param name="newAnchor">new anchor that contains the data from both
        /// anchor1 and anchor2</param>
        /// <returns>true if the anchors were merged, false otherwise
        /// </returns>
        /// <exception cref="ArgumentNullException">anchor1 or anchor2 are
        /// null</exception>
        public override bool MergeSelections(Object anchor1, Object anchor2, out Object newAnchor)
        {
            return TextSelectionHelper.MergeSelections(anchor1, anchor2, out newAnchor);
        }
 
 
        /// <summary>
        ///     Gets the tree elements spanned by the selection.
        /// </summary>
        /// <param name="selection">the selection to examine</param>
        /// <returns>a list of elements spanned by the selection; never returns
        /// null</returns>
        /// <exception cref="ArgumentNullException">selection is null</exception>
        /// <exception cref="ArgumentException">selection is of wrong type</exception>
        public override IList<DependencyObject> GetSelectedNodes(Object selection)
        {
            return TextSelectionHelper.GetSelectedNodes(selection);
        }
 
        /// <summary>
        ///     Gets the parent element of this selection.
        /// </summary>
        /// <param name="selection">the selection to examine</param>
        /// <returns>the parent element of the selection; can be null</returns>
        /// <exception cref="ArgumentNullException">selection is null</exception>
        /// <exception cref="ArgumentException">selection is of wrong type</exception>
        public override UIElement GetParent(Object selection)
        {
            return TextSelectionHelper.GetParent(selection);
        }
 
 
        /// <summary>
        ///     Gets the anchor point for the selection
        /// </summary>
        /// <param name="selection">the selection to examine</param>
        /// <returns>the anchor point of the selection; can be null</returns>
        /// <exception cref="ArgumentNullException">selection is null</exception>
        /// <exception cref="ArgumentException">selection is of wrong type</exception>
        public override Point GetAnchorPoint(Object selection)
        {
            return TextSelectionHelper.GetAnchorPoint(selection);
        }
 
 
        /// <summary>
        ///     Creates one or more locator parts representing the portion
        ///     of 'startNode' spanned by 'selection'.
        /// </summary>
        /// <param name="selection">the selection that is being processed</param>
        /// <param name="startNode">the node the locator parts should be in the
        /// context of</param>
        /// <returns>one or more locator parts representing the portion of 'startNode' spanned
        /// by 'selection'</returns>
        /// <exception cref="ArgumentNullException">startNode or selection is null</exception>
        /// <exception cref="ArgumentException">selection is of the wrong type</exception>
        public override IList<ContentLocatorPart>
            GenerateLocatorParts(Object selection, DependencyObject startNode)
        {
            ArgumentNullException.ThrowIfNull(startNode);
            ArgumentNullException.ThrowIfNull(selection);
 
            ITextPointer start;
            ITextPointer end;
            IList<TextSegment> textSegments = null;
 
            TextSelectionHelper.CheckSelection(selection, out start, out end, out textSegments);
            if (!(start is TextPointer))
                throw new ArgumentException(SR.WrongSelectionType, "selection");
 
            ITextPointer elementStart;
            ITextPointer elementEnd;
 
            // If we can't get the start/end of the node then we can't generate a locator part
            if (!GetNodesStartAndEnd(startNode, out elementStart, out elementEnd))
                return null;
 
            if (elementStart.CompareTo(end) > 0)
                throw new ArgumentException(SR.InvalidStartNodeForTextSelection, "startNode");
 
            if (elementEnd.CompareTo(start) < 0)
                throw new ArgumentException(SR.InvalidStartNodeForTextSelection, "startNode");
 
            ContentLocatorPart part = new ContentLocatorPart(CharacterRangeElementName);
 
            int startOffset = 0;
            int endOffset = 0;
            for (int i = 0; i < textSegments.Count; i++)
            {
                GetTextSegmentValues(textSegments[i], elementStart, elementEnd, out startOffset, out endOffset);
 
                part.NameValuePairs.Add(SegmentAttribute + i.ToString(NumberFormatInfo.InvariantInfo), startOffset.ToString(NumberFormatInfo.InvariantInfo) + TextSelectionProcessor.Separator + endOffset.ToString(NumberFormatInfo.InvariantInfo));
            }
 
            part.NameValuePairs.Add(CountAttribute, textSegments.Count.ToString(NumberFormatInfo.InvariantInfo));
 
            List<ContentLocatorPart> res = new List<ContentLocatorPart>(1);
 
            res.Add(part);
 
            return res;
        }
 
        /// <summary>
        ///     Creates a selection object spanning the portion of 'startNode'
        ///     specified by 'locatorPart'.
        /// </summary>
        /// <param name="locatorPart">locator part specifying data to be spanned</param>
        /// <param name="startNode">the node to be spanned by the created
        /// selection</param>
        /// <param name="attachmentLevel">set to AttachmentLevel.Full if the entire range of text
        /// was resolved, otherwise set to StartPortion, MiddlePortion, or EndPortion based on
        /// which part of the range was resolved</param>
        /// <returns>a selection spanning the portion of 'startNode' specified by
        /// 'locatorPart', null if selection described by locator part could not be
        /// recreated</returns>
        /// <exception cref="ArgumentNullException">locatorPart or startNode are
        /// null</exception>
        /// <exception cref="ArgumentException">locatorPart is of the incorrect type</exception>
        public override Object ResolveLocatorPart(ContentLocatorPart locatorPart, DependencyObject startNode, out AttachmentLevel attachmentLevel)
        {
            ArgumentNullException.ThrowIfNull(startNode);
            ArgumentNullException.ThrowIfNull(locatorPart);
 
            if (CharacterRangeElementName != locatorPart.PartType)
                throw new ArgumentException(SR.Format(SR.IncorrectLocatorPartType, $"{locatorPart.PartType.Namespace}:{locatorPart.PartType.Name}"), "locatorPart");
 
            // First we extract the offset and length of the
            // text range from the locator part.
            int startOffset = 0;
            int endOffset = 0;
 
            string stringCount = locatorPart.NameValuePairs[CountAttribute];
            if (stringCount == null)
                throw new ArgumentException(SR.Format(SR.InvalidLocatorPart, TextSelectionProcessor.CountAttribute));
            int count = Int32.Parse(stringCount, NumberFormatInfo.InvariantInfo);
 
            TextAnchor anchor = new TextAnchor();
 
            attachmentLevel = AttachmentLevel.Unresolved;
 
            for (int i = 0; i < count; i++)
            {
                GetLocatorPartSegmentValues(locatorPart, i, out startOffset, out endOffset);
 
                // Now we grab the TextRange so we can create a selection.
                // TextBox doesn't expose its internal TextRange so we use
                // its API for creating and getting the selection.
                ITextPointer elementStart;
                ITextPointer elementEnd;
                // If we can't get the start/end of the node then we can't resolve the locator part
                if (!GetNodesStartAndEnd(startNode, out elementStart, out elementEnd))
                    return null;
 
                // If the offset is not withing the element's text range we return null
                int textRangeLength = elementStart.GetOffsetToPosition(elementEnd);
                if (startOffset > textRangeLength)
                    return null;
 
                ITextPointer start = elementStart.CreatePointer(startOffset);// new TextPointer((TextPointer)elementStart, startOffset);
 
                ITextPointer end = (textRangeLength <= endOffset) ?
                    elementEnd.CreatePointer() : //new TextPointer((TextPointer)elementEnd) :
                    elementStart.CreatePointer(endOffset);// new TextPointer((TextPointer)elementStart, endOffset);
 
                //we do not process 0 length selection
                if (start.CompareTo(end) >= 0)
                    return null;
 
                anchor.AddTextSegment(start, end);
            }
 
            //we do not support 0 or negative length selection
            if (anchor.IsEmpty)
            {
                throw new ArgumentException(SR.IncorrectAnchorLength, "locatorPart");
            }
 
            attachmentLevel = AttachmentLevel.Full;
 
            if (_clamping)
            {
                ITextPointer currentStart = anchor.Start;
                ITextPointer currentEnd = anchor.End;
                IServiceProvider serviceProvider = null;
                ITextView textView = null;
 
                if (_targetPage != null)
                {
                    serviceProvider = _targetPage as IServiceProvider;
                }
                else
                {
                    FlowDocument content = currentStart.TextContainer.Parent as FlowDocument;
                    serviceProvider = PathNode.GetParent(content as DependencyObject) as IServiceProvider;
                }
 
                Invariant.Assert(serviceProvider != null, "No ServiceProvider found to get TextView from.");
                textView = serviceProvider.GetService(typeof(ITextView)) as ITextView;
                Invariant.Assert(textView != null, "Null TextView provided by ServiceProvider.");
 
                anchor = TextAnchor.TrimToIntersectionWith(anchor, textView.TextSegments);
 
                if (anchor == null)
                {
                    attachmentLevel = AttachmentLevel.Unresolved;
                }
                else
                {
                    if (anchor.Start.CompareTo(currentStart) != 0)
                    {
                        attachmentLevel &= ~AttachmentLevel.StartPortion;
                    }
                    if (anchor.End.CompareTo(currentEnd) != 0)
                    {
                        attachmentLevel &= ~AttachmentLevel.EndPortion;
                    }
                }
            }
 
            return anchor;
        }
 
        /// <summary>
        ///     Returns a list of XmlQualifiedNames representing the
        ///     the locator parts this processor can resolve/generate.
        /// </summary>
        public override XmlQualifiedName[] GetLocatorPartTypes()
        {
            return (XmlQualifiedName[])LocatorPartTypeNames.Clone();
        }
 
        #endregion Public Methods
 
        //------------------------------------------------------
        //
        //  Public Operators
        //
        //------------------------------------------------------
 
        //------------------------------------------------------
        //
        //  Public Properties
        //
        //------------------------------------------------------
 
        //------------------------------------------------------
        //
        //  Public Events
        //
        //------------------------------------------------------
        //------------------------------------------------------
        //
        //  Internal Properties
        //
        //------------------------------------------------------
        #region Internal Properties
 
        /// <summary>
        /// Controls whether or not resolving should clamp the text
        /// anchors to the visible portion of a text container.  Default
        /// value is true.
        /// </summary>
        internal bool Clamping
        {
            set
            {
                _clamping = value;
            }
        }
 
        #endregion Internal Properties
 
        //------------------------------------------------------
        //
        //  Internal Methods
        //
        //------------------------------------------------------
 
        #region Internal Methods
 
        /// <summary>
        /// Gets the smallest offset and the largest offset from all the segments defined in the locator part.
        /// </summary>
        internal static void GetMaxMinLocatorPartValues(ContentLocatorPart locatorPart, out int startOffset, out int endOffset)
        {
            ArgumentNullException.ThrowIfNull(locatorPart);
 
            string stringCount = locatorPart.NameValuePairs[CountAttribute];
            if (stringCount == null)
                throw new ArgumentException(SR.Format(SR.InvalidLocatorPart, TextSelectionProcessor.CountAttribute));
            int count = Int32.Parse(stringCount, NumberFormatInfo.InvariantInfo);
 
            startOffset = Int32.MaxValue;
            endOffset = 0;
 
            int segStart;
            int segEnd;
            for (int i = 0; i < count; i++)
            {
                GetLocatorPartSegmentValues(locatorPart, i, out segStart, out segEnd);
 
                if (segStart < startOffset)
                    startOffset = segStart;
 
                if (segEnd > endOffset)
                    endOffset = segEnd;
            }
        }
 
        /// <summary>
        ///     Set the DocumentPageView this selection processor should use
        ///     when clamping text anchors. If this target is not set then
        ///     the set of DPVs held by the viewer are used.
        /// </summary>
        internal void SetTargetDocumentPageView(DocumentPageView target)
        {
            Debug.Assert(target != null);
            _targetPage = target;
        }
 
        #endregion Internal Methods
 
        //------------------------------------------------------
        //
        //  Internal Fields
        //
        //------------------------------------------------------
 
        #region Internal Fields
 
        // Name of segment attribute
        internal const String SegmentAttribute = "Segment";
 
        // Name of segment attribute
        internal const String CountAttribute = "Count";
 
        // Name added to a LocatorPart with value "true" to mean
        // the LocatorPart matches for any overlapping LocatorPart
        internal const String IncludeOverlaps = "IncludeOverlaps";
 
        // Potential separators for values in segment name/value pairs
        internal const Char Separator = ',';
 
        // Name of locator part element
        internal static readonly XmlQualifiedName CharacterRangeElementName = new XmlQualifiedName("CharacterRange", AnnotationXmlConstants.Namespaces.BaseSchemaNamespace);
 
        #endregion Internal Fields
 
        //------------------------------------------------------
        //
        //  Private Methods
        //
        //------------------------------------------------------
 
        #region Private Methods
 
        /// <summary>
        ///     Extracts the values of attributes from a locator part.
        /// </summary>
        /// <param name="locatorPart">the locator part to extract values from</param>
        /// <param name="segmentNumber">the number of the segment to extract values for</param>
        /// <param name="startOffset">value of offset attribute</param>
        /// <param name="endOffset">value of length attribute</param>
        private static void GetLocatorPartSegmentValues(ContentLocatorPart locatorPart, int segmentNumber, out int startOffset, out int endOffset)
        {
            if (segmentNumber < 0)
                throw new ArgumentException("segmentNumber");
 
            string segmentString = locatorPart.NameValuePairs[SegmentAttribute + segmentNumber.ToString(NumberFormatInfo.InvariantInfo)];
 
            string[] values = segmentString.Split(Separator);
            if (values.Length != 2)
            {
                throw new ArgumentException(SR.Format(SR.InvalidLocatorPart, SegmentAttribute + segmentNumber.ToString(NumberFormatInfo.InvariantInfo)));
            }
 
            startOffset = Int32.Parse(values[0], NumberFormatInfo.InvariantInfo);
            endOffset = Int32.Parse(values[1], NumberFormatInfo.InvariantInfo);
        }
 
        /// <summary>
        ///     Returns the TextContainer for a node that contains text.  If the node
        ///     doesn't have a TextContainer this method returns null.
        /// </summary>
        private ITextContainer GetTextContainer(DependencyObject startNode)
        {
            Debug.Assert(startNode != null);
 
            ITextContainer textContainer = null;
            IServiceProvider serviceProvider = startNode as IServiceProvider;
            if (serviceProvider != null)
            {
                textContainer = serviceProvider.GetService(typeof(ITextContainer)) as ITextContainer;
            }
 
            if (textContainer == null)
            {
                // Special case for TextBox which doesn't implement IServiceProvider
                TextBoxBase textBox = startNode as TextBoxBase;
                if (textBox != null)
                {
                    textContainer = textBox.TextContainer;
                }
            }
 
            return textContainer;
        }
 
        /// <summary>
        ///     Returns ITextPointers positioned at the start and end of an element
        ///     that contains text.
        /// </summary>
        private bool GetNodesStartAndEnd(DependencyObject startNode, out ITextPointer start, out ITextPointer end)
        {
            start = null;
            end = null;
 
            ITextContainer textContainer = GetTextContainer(startNode);
            if (textContainer != null)
            {
                start = textContainer.Start;
                end = textContainer.End;
            }
            else
            {
                // Special case for TextElement which doesn't expose its TextContainer
                TextElement textElement = startNode as TextElement;
                if (textElement != null)
                {
                    start = textElement.ContentStart;
                    end = textElement.ContentEnd;
                }
                else
                {
                    return false;
                }
            }
            return true;
        }
 
 
        /// <summary>
        /// Gets start and end offset for a text segment but clamps those values to the start and end
        /// of a given element.  This way if a large text range is being resolved on a node that only contains
        /// a portion of the text range (such as a paragraph) the result only includes the content in that node.
        /// </summary>
        private void GetTextSegmentValues(TextSegment segment, ITextPointer elementStart, ITextPointer elementEnd, out int startOffset, out int endOffset)
        {
            startOffset = 0;
            endOffset = 0;
 
            if (elementStart.CompareTo(segment.Start) >= 0)
            {
                // segment starts before the start of the element
                startOffset = 0;
            }
            else
            {
                startOffset = elementStart.GetOffsetToPosition(segment.Start);
            }
 
            if (elementEnd.CompareTo(segment.End) >= 0)
            {
                endOffset = elementStart.GetOffsetToPosition(segment.End);
            }
            else
            {
                // segment ends after the end of the element
                endOffset = elementStart.GetOffsetToPosition(elementEnd);
            }
        }
 
        #endregion Private Methods
 
        //------------------------------------------------------
        //
        //  Private Fields
        //
        //------------------------------------------------------
 
        #region Private Fields
 
        // ContentLocatorPart types understood by this processor
        private static readonly XmlQualifiedName[] LocatorPartTypeNames =
                new XmlQualifiedName[]
                {
                    CharacterRangeElementName
                };
 
        // Optional DPV - used in printing case when there is no viewer available
        private DocumentPageView _targetPage = null;
 
        // Controls whether or not resolving clamps text anchors to
        // the visible portion of a TextContainer
        private bool _clamping = true;
 
        #endregion Private Fields
    }
}