/// <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) { if (startNode == null) throw new ArgumentNullException("startNode"); if (locatorPart == null) throw new ArgumentNullException("locatorPart"); if (CharacterRangeElementName != locatorPart.PartType) throw new ArgumentException(SR.Get(SRID.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.Get(SRID.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.Get(SRID.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> /// 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) { if (startNode == null) throw new ArgumentNullException("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.Get(SRID.StartNodeMustBeDocumentPageViewOrFixedPage), "startNode"); } if (locatorPart == null) throw new ArgumentNullException("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.Get(SRID.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> /// Merges highlights with the same color. Splits highlights with different colors /// </summary> /// <param name="service">the AnnotationService</param> /// <param name="textRange">TextRange of the new highlight</param> /// <param name="color">highlight color</param> /// <param name="author">highlight author</param> /// <param name="create">if true, this is create highlight operation, otherwise it is Clear</param> /// <returns>the annotation created, if create flag was true; null otherwise</returns> private static Annotation ProcessHighlights(AnnotationService service, ITextRange textRange, string author, Nullable<Color> color, bool create) { Invariant.Assert(textRange != null, "Parameter 'textRange' is null."); IList<IAttachedAnnotation> spannedAnnots = GetSpannedAnnotations(service); // Step one: trim all the spanned annotations so there is no overlap with the new annotation foreach (IAttachedAnnotation attachedAnnotation in spannedAnnots) { if (HighlightComponent.TypeName.Equals(attachedAnnotation.Annotation.AnnotationType)) { TextAnchor textAnchor = attachedAnnotation.FullyAttachedAnchor as TextAnchor; Invariant.Assert(textAnchor != null, "FullyAttachedAnchor must not be null."); TextAnchor copy = new TextAnchor(textAnchor); // This modifies the current fully resolved anchor copy = TextAnchor.TrimToRelativeComplement(copy, textRange.TextSegments); // If the trimming resulting in no more content being in the anchor, // delete the annotation if (copy == null || copy.IsEmpty) { service.Store.DeleteAnnotation(attachedAnnotation.Annotation.Id); continue; } // If there was some portion of content still in the anchor, // generate new locators representing the modified anchor SetAnchor(service, attachedAnnotation.Annotation, copy); } } // Step two: create new annotation if (create) { //create one annotation and return Annotation highlight = CreateHighlight(service, textRange, author, color); service.Store.AddAnnotation(highlight); return highlight; } return null; }
/// <summary> /// Process the case of Empty selection for the different annotation types /// </summary> /// <param name="selection">the selection</param> /// <param name="anchor">annotation anchor</param> /// <param name="type">annotation type</param> /// <returns></returns> private static bool CheckCaret(ITextSelection selection, TextAnchor anchor, XmlQualifiedName type) { //selection must be empty if (!selection.IsEmpty) return false; if (((anchor.Start.CompareTo(selection.Start) == 0) && (selection.Start.LogicalDirection == LogicalDirection.Forward)) || ((anchor.End.CompareTo(selection.End) == 0) && (selection.End.LogicalDirection == LogicalDirection.Backward))) return true; return false; }
/// <summary> /// Modifies the text anchor's TextSegments so all of them /// overlap with the passed in text segments. This is used /// for instance to clamp a TextAnchor to a set of visible /// text segments. If after trimming the anchor has no more /// segments, null is returned instead. Callers should /// assign the result of this method to the anchor they /// passed in. /// </summary> /// <remarks> /// Note: This method assumes textSegments is ordered and do not overlap amongs themselves /// /// The target of the method is to trim this anchor's segments to overlap with the passed in segments. /// The loop handles the following three cases - /// 1. Current segment is after other segment, the other segment doesn't contribute at all, we move to the next other segment /// 2. Current segment is before other segment, no overlap, remove current segment /// 3. Current segment starts before other segment, and ends after other segment begins, /// therefore the portion from current's start to other's start should be trimmed /// 4. Current segment starts in the middle of other segment, two possibilities /// a. current segment is completely within other segment, the whole segment overlaps /// so we move on to the next current segment /// b. current segment ends after other segment ends, we split current into the /// overlapped portion and the remainder which will be looked at separately /// </remarks> /// <param name="anchor">the anchor to trim</param> /// <param name="textSegments">collection of text segments to intersect with</param> internal static TextAnchor TrimToIntersectionWith(TextAnchor anchor, ICollection <TextSegment> textSegments) { Invariant.Assert(anchor != null, "Anchor must not be null."); Invariant.Assert(textSegments != null, "TextSegments must not be null."); textSegments = SortTextSegments(textSegments, true); TextSegment currentSegment, otherSegment = TextSegment.Null; int current = 0; IEnumerator <TextSegment> enumerator = textSegments.GetEnumerator(); bool hasMore = enumerator.MoveNext(); while (current < anchor._segments.Count && hasMore) { Invariant.Assert(otherSegment.Equals(TextSegment.Null) || otherSegment.Equals(enumerator.Current) || otherSegment.End.CompareTo(enumerator.Current.Start) <= 0, "TextSegments are overlapping or not ordered."); currentSegment = anchor._segments[current]; otherSegment = enumerator.Current; // Current segment is after other segment, so try the next other segment if (currentSegment.Start.CompareTo(otherSegment.End) >= 0) { hasMore = enumerator.MoveNext(); // point to the next other continue; // Do not increment, we are still on the same current } // Current segment is before other segment, no overlap so remove it and continue if (currentSegment.End.CompareTo(otherSegment.Start) <= 0) { anchor._segments.RemoveAt(current); continue; // Do not increment, it happens implicitly because of the remove } // // We know from here down that there is some overlap. // // Current starts before the other segment and ends after other segment begins, the first portion of current segment doesn't overlap so we remove it if (currentSegment.Start.CompareTo(otherSegment.Start) < 0) { anchor._segments[current] = CreateNormalizedSegment(otherSegment.Start, currentSegment.End); continue; // Do not increment, we need to look at this just created segment } // Current segment begins in the middle of other segment... else { // and ends after other segment does, we split current into the portion that is overlapping and the remainder if (currentSegment.End.CompareTo(otherSegment.End) > 0) { anchor._segments[current] = CreateNormalizedSegment(currentSegment.Start, otherSegment.End); // This segment will be the first one looked at next anchor._segments.Insert(current + 1, CreateNormalizedSegment(otherSegment.End, currentSegment.End)); hasMore = enumerator.MoveNext(); } // and ends at the same place as other segment, its completely overlapping, we move on to the next other else if (currentSegment.End.CompareTo(otherSegment.End) == 0) { hasMore = enumerator.MoveNext(); } // and ends within other segment, its completely overlapping, but we aren't done with other so we just continue } current++; } // If we finished and there are no more other segments, then any remaining segments // in our list must not overlap, so we remove them. if (!hasMore && current < anchor._segments.Count) { anchor._segments.RemoveRange(current, anchor._segments.Count - current); } if (anchor._segments.Count == 0) { return(null); } else { return(anchor); } }
/// <summary> /// Modifies the passed in TextAnchor to contain its relative /// complement to the set of text segments passed in. The resulting /// TextAnchor contains those segments or portions of segments that do /// not overlap with the passed in segments in anyway. If after trimming /// the anchor has no more segments, null is returned instead. Callers /// should assign the result of this method to the anchor they passed in. /// </summary> /// <param name="anchor">the anchor to trim</param> /// <param name="textSegments">the text segments to calculate relative complement with</param> /// <remarks>Note: textSegments is expected to be ordered and contain no overlapping segments</remarks> internal static TextAnchor TrimToRelativeComplement(TextAnchor anchor, ICollection <TextSegment> textSegments) { Invariant.Assert(anchor != null, "Anchor must not be null."); Invariant.Assert(textSegments != null, "TextSegments must not be null."); textSegments = SortTextSegments(textSegments, true); IEnumerator <TextSegment> enumerator = textSegments.GetEnumerator(); bool hasMore = enumerator.MoveNext(); int currentIndex = 0; TextSegment current; TextSegment otherSegment = TextSegment.Null; while (currentIndex < anchor._segments.Count && hasMore) { Invariant.Assert(otherSegment.Equals(TextSegment.Null) || otherSegment.Equals(enumerator.Current) || otherSegment.End.CompareTo(enumerator.Current.Start) <= 0, "TextSegments are overlapping or not ordered."); current = anchor._segments[currentIndex]; otherSegment = enumerator.Current; // Current segment is after other segment, no overlap // Also, done with the other segment, move to the next one if (current.Start.CompareTo(otherSegment.End) >= 0) { hasMore = enumerator.MoveNext(); continue; // No increment, still processing the current segment } // Current segment starts after other segment starts and ... if (current.Start.CompareTo(otherSegment.Start) >= 0) { // ends before other segment ends, complete overlap, remove the segment if (current.End.CompareTo(otherSegment.End) <= 0) { anchor._segments.RemoveAt(currentIndex); continue; // No increment, happens implicitly because of the removal } else { // ends after other segment, first portion of current overlaps, // create new segment from end of other segment to end of current anchor._segments[currentIndex] = CreateNormalizedSegment(otherSegment.End, current.End); // Done with the other segment, move to the next one hasMore = enumerator.MoveNext(); continue; // No increment, need to process just created segment } } // Current segment starts before other segment starts and ... else { // ends after it starts, first portion of current does not overlap, // create new segment for that portion if (current.End.CompareTo(otherSegment.Start) > 0) { anchor._segments[currentIndex] = CreateNormalizedSegment(current.Start, otherSegment.Start); // If there's any portion of current after other segment, create a new segment for that which // will be the next one processed if (current.End.CompareTo(otherSegment.End) > 0) { // Overlap ends before current segment's end, we create a new segment with the remainder of current segment anchor._segments.Insert(currentIndex + 1, CreateNormalizedSegment(otherSegment.End, current.End)); // Done with the other segment, move to the next one hasMore = enumerator.MoveNext(); } } // ends before it starts, current is completely before other, no overlap, do nothing } currentIndex++; } if (anchor._segments.Count > 0) { return(anchor); } else { return(null); } }