|
// 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:
// 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"), "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"), "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, "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, "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}"), "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)));
string[] values = segmentValue.Split(TextSelectionProcessor.Separator);
if (values.Length != 4)
throw new ArgumentException(SR.Format(SR.InvalidLocatorPart, TextSelectionProcessor.SegmentAttribute + segmentNumber.ToString(NumberFormatInfo.InvariantInfo)));
start = GetPoint(values[0], values[1]);
end = GetPoint(values[2], values[3]);
}
/// <summary>
/// Calculate Point out of string X and Y values
/// </summary>
/// <param name="xstr">x string value</param>
/// <param name="ystr">y string value</param>
/// <returns></returns>
private Point GetPoint(string xstr, string ystr)
{
Point point;
if (xstr != null && !String.IsNullOrEmpty(xstr.Trim()) && ystr != null && !String.IsNullOrEmpty(ystr.Trim()))
{
double x = Double.Parse(xstr, NumberFormatInfo.InvariantInfo);
double y = Double.Parse(ystr, 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;
}
}
int _page;
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
}
}
|