// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // // // Description: // FixedTextSelectionProcessor uses TextAnchors to represent portions // of text that are anchors. It produces FixedTextRange locator parts that are designed // specifically for use inside the DocumentViewer control. This locator part contains the // page number and the start and end points of the text selection. // FixedTextSelectionProcessor converts the text selection to FixedTextRange // // Spec: Anchoring to text in paginated docs.doc // using System.Windows; using System.Collections.ObjectModel; using System.Globalization; using System.Windows.Annotations; using System.Windows.Controls.Primitives; using System.Windows.Documents; using System.Xml; namespace MS.Internal.Annotations.Anchoring { /// <summary> /// FixedTextSelectionProcessor uses TextAnchors to represent portions /// of text that are anchors. It produces locator parts that /// represent these TextRanges by beginning and end position /// </summary> internal class FixedTextSelectionProcessor : SelectionProcessor { //------------------------------------------------------ // // Constructors // //------------------------------------------------------ #region Constructors /// <summary> /// Creates an instance of FixedTextSelectionProcessor. /// </summary> public FixedTextSelectionProcessor() { } #endregion Constructors //------------------------------------------------------ // // Public Methods // //------------------------------------------------------ #region Public Methods /// <summary> /// Merges the two anchors into one, if possible. It does not require /// the anchors to be connected. All this method does is to create a /// TextAnchor that spans the two anchors. /// </summary> /// <param name="anchor1">anchor to merge. Must be a TextAnchor. </param> /// <param name="anchor2">other anchor to merge. Must be a TextAnchor. </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> public override bool MergeSelections(Object anchor1, Object anchor2, out Object newAnchor) { return TextSelectionHelper.MergeSelections(anchor1, anchor2, out newAnchor); } /// <summary> /// Generates FixedPageProxy objects for each page, spaned by the selection /// </summary> /// <param name="selection">the selection to examine. Must implement ITextRange</param> /// <returns>a list of FixedPageProxy objects, corresponding to each page spanned by the selection; never returns /// null</returns> /// <exception cref="ArgumentNullException">selection is null</exception> /// <exception cref="ArgumentException">selection is of wrong type</exception> /// <exception cref="ArgumentException">selection start or end point can not be resolved to a page</exception> public override IList<DependencyObject> GetSelectedNodes(Object selection) { IList<TextSegment> textSegments = CheckSelection(selection); IList<DependencyObject> pageEl = new List<DependencyObject>(); Point start; Point end; foreach (TextSegment segment in textSegments) { int startPage = int.MinValue; ITextPointer startPointer = segment.Start.CreatePointer(LogicalDirection.Forward); TextSelectionHelper.GetPointerPage(startPointer, out startPage); start = TextSelectionHelper.GetPointForPointer(startPointer); if (startPage == int.MinValue) throw new ArgumentException(SR.Format(SR.SelectionDoesNotResolveToAPage, "start"), nameof(selection)); int endPage = int.MinValue; ITextPointer endPointer = segment.End.CreatePointer(LogicalDirection.Backward); TextSelectionHelper.GetPointerPage(endPointer, out endPage); end = TextSelectionHelper.GetPointForPointer(endPointer); if (endPage == int.MinValue) throw new ArgumentException(SR.Format(SR.SelectionDoesNotResolveToAPage, "end"), nameof(selection)); int firstPage = pageEl.Count; int numOfPages = endPage - startPage; Debug.Assert(numOfPages >= 0, "start page number is bigger than the end page number"); // If the first page of this segment already has an FPP, then use that one for an additional segment int i = 0; if (pageEl.Count > 0 && ((FixedPageProxy)pageEl[pageEl.Count - 1]).Page == startPage) { firstPage--; // use the existing one from the list as the first i++; // make 1 fewer FPPs } for (; i <= numOfPages; i++) { pageEl.Add(new FixedPageProxy(segment.Start.TextContainer.Parent, startPage + i)); } // If entire segment is on one page set both start/end on that page if (numOfPages == 0) { ((FixedPageProxy)pageEl[firstPage]).Segments.Add(new PointSegment(start, end)); } else { // otherwise set start on the first page and end on the last page ((FixedPageProxy)pageEl[firstPage]).Segments.Add(new PointSegment(start, PointSegment.NotAPoint)); ((FixedPageProxy)pageEl[firstPage + numOfPages]).Segments.Add(new PointSegment(PointSegment.NotAPoint, end)); } } return pageEl; } /// <summary> /// Gets the parent element of this selection. The parent element is the /// FixedPage that contains selection.Start TextPointer. /// </summary> /// <param name="selection">the selection to examine. Must implement ITextRange</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) { CheckAnchor(selection); return TextSelectionHelper.GetParent(selection); } /// <summary> /// Gets the anchor point for the selection. This is the Point that corresponds /// to the start position of the selection /// </summary> /// <param name="selection">the selection to examine. Must implement ITextRange</param> /// <returns>the anchor point of the selection; can be (double.NaN, double.NaN) if the /// selection start point is not contained in a document viewer</returns> /// <exception cref="ArgumentNullException">selection is null</exception> /// <exception cref="ArgumentException">selection is of wrong type</exception> public override Point GetAnchorPoint(Object selection) { CheckAnchor(selection); return TextSelectionHelper.GetAnchorPoint(selection); } /// <summary> /// Creates one locator part representing part of the selection /// that lies within start node /// </summary> /// <param name="selection">the selection that is being processed. Must implement ITextRange</param> /// <param name="startNode">The FixedPageProxy object, representing one page of the document</param> /// <returns>A list containing one FixedTextRange locator part</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); CheckSelection(selection); FixedPageProxy fp = startNode as FixedPageProxy; if (fp == null) throw new ArgumentException(SR.StartNodeMustBeFixedPageProxy, nameof(startNode)); ContentLocatorPart part = new ContentLocatorPart(FixedTextElementName); if (fp.Segments.Count == 0) { part.NameValuePairs.Add(TextSelectionProcessor.CountAttribute, 1.ToString(NumberFormatInfo.InvariantInfo)); part.NameValuePairs.Add(TextSelectionProcessor.SegmentAttribute + 0.ToString(NumberFormatInfo.InvariantInfo), ",,,"); } else { part.NameValuePairs.Add(TextSelectionProcessor.CountAttribute, fp.Segments.Count.ToString(NumberFormatInfo.InvariantInfo)); for (int i = 0; i < fp.Segments.Count; i++) { string value = ""; if (!double.IsNaN(fp.Segments[i].Start.X)) { value += fp.Segments[i].Start.X.ToString(NumberFormatInfo.InvariantInfo) + TextSelectionProcessor.Separator + fp.Segments[i].Start.Y.ToString(NumberFormatInfo.InvariantInfo); } else { value += TextSelectionProcessor.Separator; } value += TextSelectionProcessor.Separator; if (!double.IsNaN(fp.Segments[i].End.X)) { value += fp.Segments[i].End.X.ToString(NumberFormatInfo.InvariantInfo) + TextSelectionProcessor.Separator + fp.Segments[i].End.Y.ToString(NumberFormatInfo.InvariantInfo); } else { value += TextSelectionProcessor.Separator; } part.NameValuePairs.Add(TextSelectionProcessor.SegmentAttribute + i.ToString(NumberFormatInfo.InvariantInfo), value); } } List<ContentLocatorPart> res = new List<ContentLocatorPart>(1); res.Add(part); return res; } /// <summary> /// Creates a TextRange object spanning the portion of 'startNode' /// specified by 'locatorPart'. /// </summary> /// <param name="locatorPart">FixedTextRange locator part specifying start and end point of /// the TextRange</param> /// <param name="startNode">the FixedPage containing this locator part</param> /// <param name="attachmentLevel">set to AttachmentLevel.Full if the FixedPage for the locator /// part was found, AttachmentLevel.Unresolved otherwise</param> /// <returns>a TextRange spanning the text between start end end point in the FixedTextRange /// locator part /// , 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> /// <exception cref="ArgumentException">startNode is not a FixedPage</exception> /// <exception cref="ArgumentException">startNode does not belong to the DocumentViewer</exception> public override Object ResolveLocatorPart(ContentLocatorPart locatorPart, DependencyObject startNode, out AttachmentLevel attachmentLevel) { ArgumentNullException.ThrowIfNull(startNode); DocumentPage docPage = null; FixedPage page = startNode as FixedPage; if (page != null) { docPage = GetDocumentPage(page); } else { // If we were passed a DPV because we are walking the visual tree, // extract the DocumentPage from it; its TextView will be used to // turn coordinates into text positions DocumentPageView dpv = startNode as DocumentPageView; if (dpv != null) { docPage = dpv.DocumentPage as FixedDocumentPage; if (docPage == null) { docPage = dpv.DocumentPage as FixedDocumentSequenceDocumentPage; } } } if (docPage == null) { throw new ArgumentException(SR.StartNodeMustBeDocumentPageViewOrFixedPage, nameof(startNode)); } ArgumentNullException.ThrowIfNull(locatorPart); attachmentLevel = AttachmentLevel.Unresolved; ITextView tv = (ITextView)((IServiceProvider)docPage).GetService(typeof(ITextView)); Debug.Assert(tv != null); ReadOnlyCollection<TextSegment> ts = tv.TextSegments; //check first if a TextRange can be generated if (ts == null || ts.Count <= 0) return null; TextAnchor resolvedAnchor = new TextAnchor(); if (docPage != null) { string stringCount = locatorPart.NameValuePairs["Count"]; if (stringCount == null) throw new ArgumentException(SR.Format(SR.InvalidLocatorPart, TextSelectionProcessor.CountAttribute)); int count = Int32.Parse(stringCount, NumberFormatInfo.InvariantInfo); for (int i = 0; i < count; i++) { // First we extract the start and end Point from the locator part. Point start; Point end; GetLocatorPartSegmentValues(locatorPart, i, out start, out end); //calulate start ITextPointer ITextPointer segStart; if (double.IsNaN(start.X) || double.IsNaN(start.Y)) { //get start of the page segStart = FindStartVisibleTextPointer(docPage); } else { //convert Point to TextPointer segStart = tv.GetTextPositionFromPoint(start, true); } if (segStart == null) { //selStart can be null if there are no insertion points on this page continue; } //calulate end ITextPointer ITextPointer segEnd; if (double.IsNaN(end.X) || double.IsNaN(end.Y)) { segEnd = FindEndVisibleTextPointer(docPage); } else { //convert Point to TextPointer segEnd = tv.GetTextPositionFromPoint(end, true); } //end TP can not be null when start is not Invariant.Assert(segEnd != null, "end TP is null when start TP is not"); attachmentLevel = AttachmentLevel.Full; // Not always true right? resolvedAnchor.AddTextSegment(segStart, segEnd); } } if (resolvedAnchor.TextSegments.Count > 0) return resolvedAnchor; else return null; } /// <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 // //------------------------------------------------------ //------------------------------------------------------ // // Private Methods // //------------------------------------------------------ #region Private Methods private DocumentPage GetDocumentPage(FixedPage page) { Invariant.Assert(page != null); DocumentPage docPage = null; PageContent content = page.Parent as PageContent; if (content != null) { FixedDocument document = content.Parent as FixedDocument; // If the document is part of a FixedDocumentSequence then we want to get the // FixedDocumentSequenceDocumentPage for the FixedPage (cause its TextView is // the one we want to use). FixedDocumentSequence sequence = document.Parent as FixedDocumentSequence; if (sequence != null) { docPage = sequence.GetPage(document, document.GetIndexOfPage(page)); } else { docPage = document.GetPage(document.GetIndexOfPage(page)); } } return docPage; } /// <summary> /// Checks if the selection object satisfies the requirements /// for this processor /// </summary> /// <param name="selection">selection</param> /// <returns>ITextRange interface, implemented by the object</returns> private IList<TextSegment> CheckSelection(object selection) { ArgumentNullException.ThrowIfNull(selection); IList<TextSegment> textSegments = null; ITextPointer start = null; ITextRange textRange = selection as ITextRange; if (textRange != null) { start = textRange.Start; textSegments = textRange.TextSegments; } else { TextAnchor anchor = selection as TextAnchor; if (anchor != null) { start = anchor.Start; textSegments = anchor.TextSegments; } else { throw new ArgumentException(SR.WrongSelectionType, $"selection: type={selection.GetType()}"); } } if (!(start.TextContainer is FixedTextContainer || start.TextContainer is DocumentSequenceTextContainer)) throw new ArgumentException(SR.WrongSelectionType, $"selection: type={selection.GetType()}"); return textSegments; } /// <summary> /// Checks if the selection object satisfies the requirements /// for this processor /// </summary> /// <param name="selection">selection</param> /// <returns>ITextRange interface, implemented by the object</returns> private TextAnchor CheckAnchor(object selection) { ArgumentNullException.ThrowIfNull(selection); TextAnchor anchor = selection as TextAnchor; if (anchor == null || !(anchor.Start.TextContainer is FixedTextContainer || anchor.Start.TextContainer is DocumentSequenceTextContainer)) { throw new ArgumentException(SR.WrongSelectionType, $"selection: type={selection.GetType()}"); } return anchor; } /// <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">number of segment value to retrieve</param> /// <param name="start">the start point value based on StartXAttribute and StartYAttribute values</param> /// <param name="end">the end point value based on EndXAttribyte and EndYattribute values</param> private void GetLocatorPartSegmentValues(ContentLocatorPart locatorPart, int segmentNumber, out Point start, out Point end) { ArgumentNullException.ThrowIfNull(locatorPart); if (FixedTextElementName != locatorPart.PartType) throw new ArgumentException(SR.Format(SR.IncorrectLocatorPartType, $"{locatorPart.PartType.Namespace}:{locatorPart.PartType.Name}"), nameof(locatorPart)); string segmentValue = locatorPart.NameValuePairs[TextSelectionProcessor.SegmentAttribute + segmentNumber.ToString(NumberFormatInfo.InvariantInfo)]; if (segmentValue == null) throw new ArgumentException(SR.Format(SR.InvalidLocatorPart, TextSelectionProcessor.SegmentAttribute + segmentNumber.ToString(NumberFormatInfo.InvariantInfo))); ReadOnlySpan<char> segmentValueSpan = segmentValue.AsSpan(); Span<Range> splitRegions = stackalloc Range[5]; if (segmentValueSpan.Split(splitRegions, TextSelectionProcessor.Separator) != 4) throw new ArgumentException(SR.Format(SR.InvalidLocatorPart, TextSelectionProcessor.SegmentAttribute + segmentNumber.ToString(NumberFormatInfo.InvariantInfo))); start = GetPoint(segmentValueSpan[splitRegions[0]], segmentValueSpan[splitRegions[1]]); end = GetPoint(segmentValueSpan[splitRegions[2]], segmentValueSpan[splitRegions[3]]); } /// <summary> /// Calculates <see cref="Point"/> out of X and Y values supplied as a <see cref="string"/>. /// </summary> /// <param name="xValue">x string value</param> /// <param name="yValue">y string value</param> /// <returns>Initialized <see cref="Point"/> structure.</returns> private static Point GetPoint(ReadOnlySpan<char> xValue, ReadOnlySpan<char> yValue) { Point point; if (!xValue.Trim().IsEmpty && !yValue.Trim().IsEmpty) { double x = double.Parse(xValue, NumberFormatInfo.InvariantInfo); double y = double.Parse(yValue, NumberFormatInfo.InvariantInfo); point = new Point(x, y); } else { point = new Point(double.NaN, double.NaN); } return point; } /// <summary> /// Gets the first visible TP on a DocumentPage /// </summary> /// <param name="documentPage">document page</param> /// <returns>The first visible TP or null if no visible TP on this page</returns> private static ITextPointer FindStartVisibleTextPointer(DocumentPage documentPage) { ITextPointer start, end; if (!GetTextViewRange(documentPage, out start, out end)) return null; if (!start.IsAtInsertionPosition && !start.MoveToNextInsertionPosition(LogicalDirection.Forward)) { //there is no insertion point in this direction return null; } //check if it is outside of the page if (start.CompareTo(end) > 0) return null; return start; } /// <summary> /// Gets the last visible TP on a DocumentPage /// </summary> /// <param name="documentPage">document page</param> /// <returns>The last visible TP or null if no visible TP on the page</returns> private static ITextPointer FindEndVisibleTextPointer(DocumentPage documentPage) { ITextPointer start, end; if (!GetTextViewRange(documentPage, out start, out end)) return null; if (!end.IsAtInsertionPosition && !end.MoveToNextInsertionPosition(LogicalDirection.Backward)) { //there is no insertion point in this direction return null; } //check if it is outside of the page if (start.CompareTo(end) > 0) return null; return end; } /// <summary> /// Gets first and last TP on a documentPage. /// </summary> /// <param name="documentPage">the document page</param> /// <param name="start">start TP</param> /// <param name="end">end TP</param> /// <returns>true if there aretext segments on this page, otherwise false</returns> private static bool GetTextViewRange(DocumentPage documentPage, out ITextPointer start, out ITextPointer end) { ITextView textView; start = end = null; // Missing pages can't produce TextPointers Invariant.Assert(documentPage != DocumentPage.Missing); textView = ((IServiceProvider)documentPage).GetService(typeof(ITextView)) as ITextView; Invariant.Assert(textView != null, "DocumentPage didn't provide a TextView."); //check if there is any content if ((textView.TextSegments == null) || (textView.TextSegments.Count == 0)) return false; start = textView.TextSegments[0].Start.CreatePointer(LogicalDirection.Forward); end = textView.TextSegments[textView.TextSegments.Count - 1].End.CreatePointer(LogicalDirection.Backward); Debug.Assert((start != null) && (end != null), "null start/end TextPointer on a non empty page"); return true; } #endregion Private Methods //------------------------------------------------------ // // Private Fields // //------------------------------------------------------ #region Private Fields // Name of locator part element private static readonly XmlQualifiedName FixedTextElementName = new XmlQualifiedName("FixedTextRange", AnnotationXmlConstants.Namespaces.BaseSchemaNamespace); // ContentLocatorPart types understood by this processor private static readonly XmlQualifiedName[] LocatorPartTypeNames = new XmlQualifiedName[] { FixedTextElementName }; #endregion Private Fields #region Internal Classes /// <summary> /// Returned by GetSelectedNodes - one object per page spanned by the selection /// </summary> internal sealed class FixedPageProxy : DependencyObject { public FixedPageProxy(DependencyObject parent, int page) { SetValue(PathNode.HiddenParentProperty, parent); _page = page; } public int Page { get { return _page; } } public IList<PointSegment> Segments { get { return _segments; } } private int _page; private IList<PointSegment> _segments = new List<PointSegment>(1); } /// <summary> /// PointSegment represents a segment in fixed content with start and end points. /// </summary> internal sealed class PointSegment { /// <summary> /// Creates a PointSegment with the given points. /// </summary> internal PointSegment(Point start, Point end) { _start = start; _end = end; } /// <summary> /// The start point of the segment /// </summary> public Point Start { get { return _start; } } /// <summary> /// The end point of the segment /// </summary> public Point End { get { return _end; } } /// <summary> /// Used to represent a non-existent point - for instance for a segment /// which spans an entire page there is no starting point. /// </summary> public static readonly Point NotAPoint = new Point(double.NaN, double.NaN); private Point _start; private Point _end; } #endregion Internal Classes } } |