Code:
/ Dotnetfx_Vista_SP2 / Dotnetfx_Vista_SP2 / 8.0.50727.4016 / DEVDIV / depot / DevDiv / releases / Orcas / QFE / wpf / src / Framework / System / Windows / Documents / TextRangeEdit.cs / 1 / TextRangeEdit.cs
//---------------------------------------------------------------------------- // // File: TextRangeEdit.cs // // Copyright (C) Microsoft Corporation. All rights reserved. // // Description: Static internal class providing a set of // helpoer methods for text editing operations // //--------------------------------------------------------------------------- namespace System.Windows.Documents { using System; using MS.Internal; using System.Windows.Controls; ////// The TextRange class represents a pair of TextPositions, with many /// rich text editing operations exposed. /// internal static class TextRangeEdit { // ------------------------------------------------------------------- // // Internal Methods // // ------------------------------------------------------------------- #region Internal Methods internal static TextElement InsertElementClone(TextPointer start, TextPointer end, TextElement element) { TextElement newElement = (TextElement)Activator.CreateInstance(element.GetType()); // Copy properties to the newElement newElement.TextContainer.SetValues(newElement.ContentStart, element.GetLocalValueEnumerator()); newElement.Reposition(start, end); return newElement; } // .................................................................... // // Character Formatting // // .................................................................... #region Character Formatting ////// internal static TextPointer SplitFormattingElements(TextPointer splitPosition, bool keepEmptyFormatting) { return SplitFormattingElements(splitPosition, keepEmptyFormatting, /*limitingAncestor*/null); } internal static TextPointer SplitFormattingElement(TextPointer splitPosition, bool keepEmptyFormatting) { Invariant.Assert(splitPosition.Parent != null && TextSchema.IsMergeableInline(splitPosition.Parent.GetType())); Inline inline = (Inline)splitPosition.Parent; // Create a movable copy of a splitPosition if (splitPosition.IsFrozen) { splitPosition = new TextPointer(splitPosition); } if (!keepEmptyFormatting && splitPosition.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart) { // The first part of element is empty. We are allowed to remove empty formatting elements, // so we can simply move splitPotision outside of the element and we are done splitPosition.MoveToPosition(inline.ElementStart); } else if (!keepEmptyFormatting && splitPosition.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd) { // The second part of element is empty. We are allowed to remove empty formatting elements, // so we can simply move splitPotision outside of the element and we are done. splitPosition.MoveToPosition(inline.ElementEnd); } else { splitPosition = SplitElement(splitPosition); } return splitPosition; } // Compares a set of inheritable properties taken from two objects private static bool InheritablePropertiesAreEqual(Inline firstInline, Inline secondInline) { Invariant.Assert(firstInline != null, "null check: firstInline"); Invariant.Assert(secondInline != null, "null check: secondInline"); // Compare inheritable properties DependencyProperty[] inheritableProperties = TextSchema.GetInheritableProperties(typeof(Inline)); for (int i = 0; i < inheritableProperties.Length; i++) { DependencyProperty property = inheritableProperties[i]; if (TextSchema.IsStructuralCharacterProperty(property)) { if (firstInline.ReadLocalValue(property) != DependencyProperty.UnsetValue || secondInline.ReadLocalValue(property) != DependencyProperty.UnsetValue) { return false; } } else { if (!TextSchema.ValuesAreEqual(firstInline.GetValue(property), secondInline.GetValue(property))) { return false; } } } return true; } // Compares all character formatting properties for two elements. // Returns true if all known properties have equal values, false otherwise. // Note that only statically known character formatting properties // are taken into account. We intentionally ignore all other properties, // because TextEditor is not aware (in general) about their semantics, // and considers unsafe to duplicate them freely. // Ignorance means deletion, which is considered as safer approach. private static bool CharacterPropertiesAreEqual(Inline firstElement, Inline secondElement) { Invariant.Assert(firstElement != null, "null check: firstElement"); if (secondElement == null) { return false; } DependencyProperty[] noninheritableProperties = TextSchema.GetNoninheritableProperties(typeof(Span)); for (int i = 0; i < noninheritableProperties.Length; i++) { DependencyProperty property = noninheritableProperties[i]; if (!TextSchema.ValuesAreEqual(firstElement.GetValue(property), secondElement.GetValue(property))) { return false; } } if (!InheritablePropertiesAreEqual(firstElement, secondElement)) { return false; } return true; } ////// /// Checks if scoping element is empty formatting. /// It must be removed if not situated inside of empty block. /// /// /// TextPointer scoped by the allegedly empty formatting element(s). /// ////// true if at least one empty formatting element was extracted. /// private static bool ExtractEmptyFormattingElements(TextPointer position) { bool elementsWereExtracted = false; Inline inline = position.Parent as Inline; if (inline != null && inline.IsEmpty) { // Delete any empty non-formatting element. // We can get here if an IME deletes the UIElement from inside an InlineUIContainer. while (inline != null && inline.IsEmpty && !TextSchema.IsFormattingType(inline.GetType())) { inline.Reposition(null, null); elementsWereExtracted = true; inline = position.Parent as Inline; } // Start with removing empty Runs and Spans unconditionally. // If it is an empty non-derived Run or Span with no local properties on it - it's safe to delete it. // It does not have any formatting or any other meaning, while it can be implicitely // re-inserted when necessary. So remove it to minimize resulting xaml. while ( inline != null && inline.IsEmpty && (inline.GetType() == typeof(Run) || inline.GetType() == typeof(Span)) && !HasWriteableLocalPropertyValues(inline)) { inline.Reposition(null, null); elementsWereExtracted = true; inline = position.Parent as Inline; } // Continue deleting empty inlines that are neighbored by other formatting elements, // that make them inaccessible for caret position while (inline != null && inline.IsEmpty && ((inline.NextInline != null && TextSchema.IsFormattingType(inline.NextInline.GetType())) || (inline.PreviousInline != null && TextSchema.IsFormattingType(inline.PreviousInline.GetType())))) { inline.Reposition(null, null); elementsWereExtracted = true; inline = position.Parent as Inline; } } return elementsWereExtracted; } ////// Applies a property to a range between start and end positions. /// /// /// TextPointer identifying start of affected range. /// /// /// TextPointer identifying end of affected range. /// /// /// A dependency property whose value is supposed to applied to a range. /// /// /// A value for a property to apply. /// /// /// Specifies how to use the value - as absolute, as increment or a decrement. /// internal static void SetInlineProperty(TextPointer start, TextPointer end, DependencyProperty formattingProperty, object value, PropertyValueAction propertyValueAction) { // Check for corner case when we have siple text run with all properties set as requested. // This case is iportant optimization for Backspace-Type scenario, when Springload formatting applies for nothing for 50 properties if (start.CompareTo(end) >= 0 || propertyValueAction == PropertyValueAction.SetValue && start.Parent is Run && start.Parent == end.Parent && TextSchema.ValuesAreEqual(start.Parent.GetValue(formattingProperty), value)) { return; } // Remove unnecessary spans on range ends - to optimize resulting markup RemoveUnnecessarySpans(start); RemoveUnnecessarySpans(end); if (TextSchema.IsStructuralCharacterProperty(formattingProperty)) { SetStructuralInlineProperty(start, end, formattingProperty, value); } else { SetNonStructuralInlineProperty(start, end, formattingProperty, value, propertyValueAction); } } // Merges inline elements with equivalent formatting properties at a given position // Returns true if some changes happened at this position, false otherwise internal static bool MergeFormattingInlines(TextPointer position) { // Remove unnecessary Spans around this position RemoveUnnecessarySpans(position); // Delete empty formatting elements at this position (if any) ExtractEmptyFormattingElements(position); // Skip formatting tags towards potential merging position while (position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && TextSchema.IsMergeableInline(position.Parent.GetType())) { position = ((Inline)position.Parent).ElementStart; } while (position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd && TextSchema.IsMergeableInline(position.Parent.GetType())) { position = ((Inline)position.Parent).ElementEnd; } // Merge formatting Inlines at this position Inline firstInline, secondInline; bool merged = false; while ( position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementEnd && position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementStart && (firstInline = position.GetAdjacentElement(LogicalDirection.Backward) as Inline) != null && (secondInline = position.GetAdjacentElement(LogicalDirection.Forward) as Inline) != null) { if (TextSchema.IsFormattingType(firstInline.GetType()) && firstInline.TextRange.IsEmpty) { firstInline.RepositionWithContent(null); merged = true; } else if (TextSchema.IsFormattingType(secondInline.GetType()) && secondInline.TextRange.IsEmpty) { secondInline.RepositionWithContent(null); merged = true; } else if (TextSchema.IsKnownType(firstInline.GetType()) && TextSchema.IsKnownType(secondInline.GetType()) && (firstInline is Run && secondInline is Run || firstInline is Span && secondInline is Span) && TextSchema.IsMergeableInline(firstInline.GetType()) && TextSchema.IsMergeableInline(secondInline.GetType()) && CharacterPropertiesAreEqual(firstInline, secondInline)) { firstInline.Reposition(firstInline.ElementStart, secondInline.ElementEnd); secondInline.Reposition(null, null); merged = true; } else { break; } } // Now that Inlines have been merged we can try to optimize tree structure // by eliminating some unecessary wrapping Inlines if (merged) { RemoveUnnecessarySpans(position); } return merged; } // Inspects the tree up from a given position to find Span elements // wrapping exactly one other Span or Run - and removes them // after transferring all affected properties into inner element. private static void RemoveUnnecessarySpans(TextPointer position) { Inline inline = position.Parent as Inline; while (inline != null) { if (inline.Parent != null && TextSchema.IsMergeableInline(inline.Parent.GetType()) && TextSchema.IsKnownType(inline.Parent.GetType()) && inline.ElementStart.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && inline.ElementEnd.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd) { // Parent of this inline can be deleted. Let's delete it. Span parentSpan = (Span)inline.Parent; if (parentSpan.Parent == null) { break; } // We are going to delete a parent of this inline as it wraps only one child. // Before deleting we need to transfer all properties that are affected by that parent inline. // Transfer inheritable properties DependencyProperty[] inheritableProperties = TextSchema.GetInheritableProperties(typeof(Span)); for (int i = 0; i < inheritableProperties.Length; i++) { DependencyProperty property = inheritableProperties[i]; object inlineValue = inline.GetValue(property); object parentSpanValue = parentSpan.GetValue(property); if (!TextSchema.ValuesAreEqual(inlineValue, parentSpanValue)) { // Inner inline sets its own value for this property. We don't need to transfer it. continue; } object outerValue = parentSpan.Parent.GetValue(property); if (!TextSchema.ValuesAreEqual(inlineValue, outerValue)) { inline.SetValue(property, parentSpanValue); } } // Transfer non-inheritable properties // It only aims for the specific set of non-inheritable properties defined in TextSchema. // These properties are safe to be transferred from outer scope to inner scope. DependencyProperty[] nonInheritableProperties = TextSchema.GetNoninheritableProperties(typeof(Span)); for (int i = 0; i < nonInheritableProperties.Length; i++) { DependencyProperty property = nonInheritableProperties[i]; bool hasModifiers; // Check if the property value is default and not animated/coerced/data-bound. bool isParentValueDefault = ( parentSpan.GetValueSource(property, null, out hasModifiers) == BaseValueSourceInternal.Default && !hasModifiers ); bool isInlineValueDefault = ( inline.GetValueSource(property, null, out hasModifiers) == BaseValueSourceInternal.Default && !hasModifiers ); if (isInlineValueDefault && !isParentValueDefault) { inline.SetValue(property, parentSpan.GetValue(property)); } } // We can now remove the wrapping element parentSpan.Reposition(null, null); } else { // Parent of this inline cannot be deleted. Let's see what we can do with its parent inline = inline.Parent as Inline; } } } // Removes inline properties that affect formatting from the given range internal static void CharacterResetFormatting(TextPointer start, TextPointer end) { if (start.CompareTo(end) < 0) { // Split formatting elements at range boundaries start = SplitFormattingElements(start, /*keepEmptyFormatting:*/false, /*preserveStructuralFormatting*/true, /*limitingAncestor*/null); end = SplitFormattingElements(end, /*keepEmptyFormatting:*/false, /*preserveStructuralFormatting*/true, /*limitingAncestor*/null); while (start.CompareTo(end) < 0) { if (start.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart) { // When entering a next element check whether we should clear its inline properties. TextElement parent = (TextElement)start.Parent; // Note we do cleaning for Inline elements only - so properties set on Paragraphs // and other blocks will stay unchanged even if they set as local value. if (parent is Span && parent.ContentEnd.CompareTo(end) > 0) { // Preserve Hyperlink/Span properties when it is partially selected } // We can't assume that custom types derived from Span, once their formatting // properties are removed, can be transformed into a Span. So treat custom // types as inlines, even if they're derived from Span. else if (parent is Span && TextSchema.IsKnownType(parent.GetType())) { // Remember a position to merge inlines TextPointer mergePosition = parent.ElementStart; // Preserve only non-formatting properties of original span element. Span newSpan = TransferNonFormattingInlineProperties((Span)parent); if (newSpan != null) { newSpan.Reposition(parent.ElementStart, parent.ElementEnd); mergePosition = newSpan.ElementStart; } // Throw away original span parent.Reposition(null, null); // Now that content has changed, we must try to merge inlines at this position MergeFormattingInlines(mergePosition); } else if (parent is Inline) { ClearFormattingInlineProperties((Inline)parent); // Now that properties may be removed we must try to merge this element with a preceding one MergeFormattingInlines(parent.ElementStart); } } start = start.GetNextContextPosition(LogicalDirection.Forward); } // At the end try ro merge elements at end position MergeFormattingInlines(end); } } // Helper to clear formatting properties from passed inline element, preserving only non-formatting ones private static void ClearFormattingInlineProperties(Inline inline) { // Clear all properties from this inline element LocalValueEnumerator properties = inline.GetLocalValueEnumerator(); while (properties.MoveNext()) { DependencyProperty property = properties.Current.Property; // Skip readonly and non-formatting properties if (property.ReadOnly || TextSchema.IsNonFormattingCharacterProperty(property)) { continue; } inline.ClearValue(properties.Current.Property); } } // When source span has only character formatting properties, returns null. // Otherwise, when source span has at least one non-formatting character property (such as FlowDirection), // this helper returns a Span element preserving only such properties from source span. private static Span TransferNonFormattingInlineProperties(Span source) { Span span = null; DependencyProperty[] nonFormattingCharacterProperties = TextSchema.GetNonFormattingCharacterProperties(); for (int i = 0; i < nonFormattingCharacterProperties.Length; i++) { object value = source.GetValue(nonFormattingCharacterProperties[i]); object outerContextValue = ((ITextPointer)source.ElementStart).GetValue(nonFormattingCharacterProperties[i]); if (!TextSchema.ValuesAreEqual(value, outerContextValue)) { if (span == null) { span = new Span(); } span.SetValue(nonFormattingCharacterProperties[i], value); } } return span; } #endregion Character Formatting #region Paragraph Editing // .................................................................... // // Paragraph Editing // // .................................................................... // Splits the parent of the given breakPosition into two // elements with equivalent set of properties. internal static TextPointer SplitElement(TextPointer position) { TextElement element = (TextElement)position.Parent; if (position.IsFrozen) { position = new TextPointer(position); } TextElement newElement; if (position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd) { // A simple case when the new element can be added after the old one newElement = InsertElementClone(element.ElementEnd, element.ElementEnd, element); position.MoveToPosition(element.ElementEnd); } else if (position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart) { newElement = InsertElementClone(element.ElementStart, element.ElementStart, element); position.MoveToPosition(element.ElementStart); } else { newElement = InsertElementClone(position, element.ContentEnd, element); // Reposition the old element to the first half of content element.Reposition(element.ContentStart, newElement.ElementStart); position.MoveToPosition(element.ElementEnd); } Invariant.Assert(position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementEnd, "position must be after ElementEnd"); Invariant.Assert(position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementStart, "position must be before ElementStart"); return position; } ////// Insert paragraph break at the End position of a range. /// It only affects specified position - not a whole range. /// So it is essentially TextContainer-level (low-level) operation. /// /// /// Position at which the content should be split into two paragraphs. /// After the operation breakPosition moved into a beginning of the /// second paragraph after all opening tags created by splitting /// (this position may be not-normalized though if there are some /// other opening formatting tags following the position - this may /// be important for reading from xml when pasting point was before /// some opening formatting tags but after non-whitespace characters). /// /// /// True means that resulting TextPointer must be moved into the second paragraph. /// False means that resulting pointer remains in a non-normalized position /// between two paragraphs (or list items). /// ////// This function could be implemented from TextContainer class. /// ////// If position passed was in paragraph content, returns a TextPointer /// at an ContentStart of the second paragraph. /// If position passed was at a structural boundary (specifically table row end, /// block ui container start/end or before first table in a collection of blocks), /// then an implicit paragraph is inserted at the boundary and a position at its /// ContentStart is returned. /// internal static TextPointer InsertParagraphBreak(TextPointer position, bool moveIntoSecondParagraph) { Invariant.Assert(TextSchema.IsValidChildOfContainer(position.TextContainer.Parent.GetType(), typeof(Paragraph))); bool structuralBoundaryCrossed = TextPointerBase.IsAtRowEnd(position) || TextPointerBase.IsBeforeFirstTable(position) || TextPointerBase.IsInBlockUIContainer(position); if (position.Paragraph == null) { // Ensure insertion position, in case original position is not in text content. position = TextRangeEditTables.EnsureInsertionPosition(position); } Inline ancestor = position.GetNonMergeableInlineAncestor(); if (ancestor != null) { Invariant.Assert(TextPointerBase.IsPositionAtNonMergeableInlineBoundary(position), "Position must be at hyperlink boundary!"); // If position is at a hyperlink boundary, move outside hyperlink element scope // so that we can successfuly split formatting elements upto paragraph ancestor. position = position.IsAtNonMergeableInlineStart ? ancestor.ElementStart : ancestor.ElementEnd; } Paragraph paragraph = position.Paragraph; Invariant.Assert(paragraph != null, "Position must be in paragraph scope"); if (structuralBoundaryCrossed) { // In case structural boundary was crossed, an implicit paragraph was inserted in EnsureInsertionPosition. // No need to insert another paragraph break. return position; } TextPointer breakPosition = position; // Split all inline elements up to this paragraph breakPosition = SplitFormattingElements(breakPosition, /*keepEmptyFormatting:*/true); Invariant.Assert(breakPosition.Parent == paragraph, "breakPosition must be in paragraph scope after splitting formatting elements"); // Decide whether we need to split ListItem around this paragraph (if any). // We are splitting a list item if this paragraph is the only paragraph in a list item. // Otherwise we simply produce new paragraphs within the same list item. bool needToSplitListItem = TextPointerBase.GetImmediateListItem(paragraph.ContentStart) != null; breakPosition = SplitElement(breakPosition); // Also split ListItem (if any) if (needToSplitListItem) { Invariant.Assert(breakPosition.Parent is ListItem, "breakPosition must be in ListItem scope"); breakPosition = SplitElement(breakPosition); } if (moveIntoSecondParagraph) { // Move breakPosition inside of the second paragraph while (!(breakPosition.Parent is Paragraph) && breakPosition.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementStart) { breakPosition = breakPosition.GetNextContextPosition(LogicalDirection.Forward); } // Normalize with forward gravity breakPosition = breakPosition.GetInsertionPosition(LogicalDirection.Forward); } return breakPosition; } ////// Insert a LineBreak element at the given position. /// If position's parent is a Paragraph or Span, simply insert a LineBreak element at this position. /// Otherwise, ensure insertion position and insert a LineBreak element at insertion position in text content. /// /// /// ////// TextPointer positioned in the beginning of a Run immediately following a LineBreak inserted. /// internal static TextPointer InsertLineBreak(TextPointer position) { if (!TextSchema.IsValidChild(/*position*/position, /*childType*/typeof(LineBreak))) { // Ensure insertion position, in case position's parent is not a paragraph/span element. position = TextRangeEditTables.EnsureInsertionPosition(position); } if (TextSchema.IsInTextContent(position)) { // Split parent Run element, if position is inside of Run scope. position = SplitElement(position); } Invariant.Assert(TextSchema.IsValidChild(/*position*/position, /*childType*/typeof(LineBreak)), "position must be in valid scope now (span/paragraph) to insert a LineBreak element"); LineBreak lineBreak = new LineBreak(); position.InsertTextElement(lineBreak); return lineBreak.ElementEnd.GetInsertionPosition(LogicalDirection.Forward); } ////// Applies formatting properties for whole block elements. /// /// /// a position within first block in sequence /// /// /// a positionn within last block in sequence /// /// /// property changed on blocks /// /// /// value for the property /// internal static void SetParagraphProperty(TextPointer start, TextPointer end, DependencyProperty property, object value) { SetParagraphProperty(start, end, property, value, PropertyValueAction.SetValue); } ////// Applies formatting properties for whole block elements. /// /// /// a position within first block in sequence /// /// /// a positionn within last block in sequence /// /// /// property changed on blocks /// /// /// value for the property /// /// /// Specifies how to use the value - as absolute, as increment or a decrement. /// internal static void SetParagraphProperty(TextPointer start, TextPointer end, DependencyProperty property, object value, PropertyValueAction propertyValueAction) { Invariant.Assert(start != null, "null check: start"); Invariant.Assert(end != null, "null check: end"); Invariant.Assert(start.CompareTo(end) <= 0, "expecting: start <= end"); Invariant.Assert(property != null, "null check: property"); // Exclude last opening tag to avoid affecting a paragraph following the selection end = (TextPointer)TextRangeEdit.GetAdjustedRangeEnd(start, end); // Expand start pointer to the beginning of the first paragraph/blockuicontainer Block startParagraphOrBlockUIContainer = start.ParagraphOrBlockUIContainer; if (startParagraphOrBlockUIContainer != null) { start = startParagraphOrBlockUIContainer.ContentStart; } // Applying FlowDirection requires splitting all containing lists on the range boundaries // because the property is applied to whole List element (to affect bullet appearence) if (property == Block.FlowDirectionProperty) { // Split any boundary lists if needed. // We want to maintain the invariant that all lists and paragraphs within a list, have the same FlowDirection value. // If paragraph FlowDirection command requests a different value of FlowDirection on parts of a list, // we split the list to maintain this invariant. if (!TextRangeEditLists.SplitListsForFlowDirectionChange(start, end, value)) { // If lists at start and end cannot be split successfully, we cannot apply FlowDirection property to the paragraph content. return; } // And expand range start to the beginning of the containing list ListItem listItem = start.GetListAncestor(); if (listItem != null && listItem.List != null) { start = listItem.List.ElementStart; } } // Walk all paragraphs in the affected segment. For FlowDirection property, also walk lists. SetParagraphPropertyWorker(start, end, property, value, propertyValueAction); } // Worker for SetParagraphProperty, iterates over Blocks recursively. private static void SetParagraphPropertyWorker(TextPointer start, TextPointer end, DependencyProperty property, object value, PropertyValueAction propertyValueAction) { Block block = GetNextBlock(start, end); while (block != null) { if (TextSchema.IsParagraphOrBlockUIContainer(block.GetType())) { // Get the parent to check the parent FlowDirection with current DependencyObject parent = start.TextContainer.Parent; SetPropertyOnParagraphOrBlockUIContainer(parent, block, property, value, propertyValueAction); // Go to paragraph/BUIC end position, normalize forward start = block.ElementEnd.GetPositionAtOffset(0, LogicalDirection.Forward); } else if (block is List) { // Apply property value to content first, recursively, since // (potentially) setting FlowDirection on the parent List will // affect child elements. TextPointer contentStart = block.ContentStart.GetPositionAtOffset(0, LogicalDirection.Forward); // Normalize forward; contentStart = contentStart.GetNextContextPosition(LogicalDirection.Forward); // Leave scope of initial List. TextPointer contentEnd = block.ContentEnd; SetParagraphPropertyWorker(contentStart, contentEnd, property, value, propertyValueAction); // Special cases for applying paragraph properties to Lists if (property == Block.FlowDirectionProperty) { // Set FlowDirection property on List SetPropertyValue(block, property, /*currentValue:*/block.GetValue(property), /*newValue:*/value); // For flow direction command, we also swap Left and Right margins of the list. // This ensures indentation is mirrored correctly. SwapBlockLeftAndRightMargins(block); } // Go to end position, normalize forward. start = block.ElementEnd.GetPositionAtOffset(0, LogicalDirection.Forward); } block = GetNextBlock(start, end); } } // Helper for SetParagraphProperty -- applies given property value to passed block element. private static void SetPropertyOnParagraphOrBlockUIContainer(DependencyObject parent, Block block, DependencyProperty property, object value, PropertyValueAction propertyValueAction) { // Get the parent flow direction FlowDirection parentFlowDirection; if (parent != null) { parentFlowDirection = (FlowDirection)parent.GetValue(FrameworkElement.FlowDirectionProperty); } else { parentFlowDirection = (FlowDirection)FrameworkElement.FlowDirectionProperty.GetDefaultValue(typeof(FrameworkElement)); } // Some of paragraph operations depend on its flow direction, so get it first. FlowDirection flowDirection = (FlowDirection)block.GetValue(Block.FlowDirectionProperty); // Inspect a property value for this paragraph object currentValue = block.GetValue(property); object newValue = value; // If we're setting a structural property on a Paragraph, we need to preserve // the current value on its children. PreserveBlockContentStructuralProperty(block, property, currentValue, value); if (property.PropertyType == typeof(Thickness)) { // For Margin, Padding, Border - apply the following logic: Invariant.Assert(currentValue is Thickness, "Expecting the currentValue to be of Thinkness type"); Invariant.Assert(newValue is Thickness, "Expecting the newValue to be of Thinkness type"); newValue = ComputeNewThicknessValue((Thickness)currentValue, (Thickness)newValue, parentFlowDirection, flowDirection, propertyValueAction); } else if (property == Paragraph.TextAlignmentProperty) { Invariant.Assert(value is TextAlignment, "Expecting TextAlignment as a value of a Paragraph.TextAlignmentProperty"); // TextAlignment must be reverted for RightToLeft flow direction newValue = ComputeNewTextAlignmentValue((TextAlignment)value, flowDirection); // For BlockUIContainer text alignment must be translated into // HorizontalAlignment of the child embedded object. if (block is BlockUIContainer) { UIElement embeddedElement = ((BlockUIContainer)block).Child; if (embeddedElement != null) { HorizontalAlignment horizontalAlignment = GetHorizontalAlignmentFromTextAlignment((TextAlignment)newValue); // Create an undo unit for property change on embedded framework element. UIElementPropertyUndoUnit.Add(block.TextContainer, embeddedElement, FrameworkElement.HorizontalAlignmentProperty, horizontalAlignment); embeddedElement.SetValue(FrameworkElement.HorizontalAlignmentProperty, horizontalAlignment); } } } else if (currentValue is double) { newValue = NewValue((double)currentValue, (double)newValue, propertyValueAction); } SetPropertyValue(block, property, currentValue, newValue); if (property == Block.FlowDirectionProperty) { // For flow direction command, we also swap Left and Right margins of the paragraph. // This ensures indentation is mirrored correctly. SwapBlockLeftAndRightMargins(block); } } // Helper for SetPropertyOnParagraphOrBlockUIContainer. // // When setting a structural property on a Block, we must be careful to preserve // the current value on its children. // // A structural character property is more strict for its scope than other (non-structural) inline properties (such as fontweight). // While the associativity rule holds true for non-structural properties when there values are equal, // (FontWeight)A (FontWeight)B == (FontWeight) AB // this does not hold true for structual properties even when there values may be equal, // (FlowDirection)A (FlowDirection)B != (FlowDirection)A B private static void PreserveBlockContentStructuralProperty(Block block, DependencyProperty property, object currentValue, object newValue) { Paragraph paragraph = block as Paragraph; if (paragraph != null && TextSchema.IsStructuralCharacterProperty(property) && !TextSchema.ValuesAreEqual(currentValue, newValue)) { // First drill down to the first run of multiple children, or the first // single child with a local value. Inline firstChild = paragraph.Inlines.FirstInline; Inline lastChild = paragraph.Inlines.LastInline; while (firstChild != null && firstChild == lastChild && firstChild is Span && !HasLocalPropertyValue(firstChild, property)) { firstChild = ((Span)firstChild).Inlines.FirstInline; lastChild = ((Span)lastChild).Inlines.LastInline; } // Set the old value on the existing content. if (firstChild != lastChild) { Inline nextChild; do { // Find a run of children with the same property value. object firstChildValue = firstChild.GetValue(property); lastChild = firstChild; while (true) { nextChild = (Inline)lastChild.NextElement; if (nextChild == null) break; if (!TextSchema.ValuesAreEqual(nextChild.GetValue(property), firstChildValue)) break; lastChild = nextChild; } if (TextSchema.ValuesAreEqual(firstChildValue, currentValue)) { if (firstChild != lastChild) { // Wrap multiple children in a new Span with the old value. TextPointer start = firstChild.ElementStart.GetFrozenPointer(LogicalDirection.Backward); TextPointer end = lastChild.ElementEnd.GetFrozenPointer(LogicalDirection.Forward); // Because SetStructuralInlineProperty doesn't know that we're about to change the Paragraph's // property value, it will optimize away Spans. We still want to use it though, to canonicalize // the content. SetStructuralInlineProperty(start, end, property, currentValue); firstChild = (Inline)start.GetAdjacentElement(LogicalDirection.Forward); lastChild = (Inline)end.GetAdjacentElement(LogicalDirection.Backward); if (firstChild != lastChild) { Span span = firstChild.Parent as Span; if (span == null || span.Inlines.FirstInline != firstChild || span.Inlines.LastInline != lastChild) { span = new Span(firstChild.ElementStart, lastChild.ElementEnd); } span.SetValue(property, currentValue); } } if (firstChild == lastChild) { SetStructuralPropertyOnInline(firstChild, property, currentValue); } } firstChild = nextChild; } while (firstChild != null); } else { // If the only child is a Run, set the value directly. // Otherwise there's no need to set the value. SetStructuralPropertyOnInline(firstChild, property, currentValue); } } } // Helper for PreserveBlockContentStructuralProperty. private static void SetStructuralPropertyOnInline(Inline inline, DependencyProperty property, object value) { if (inline is Run && !inline.IsEmpty && !HasLocalPropertyValue(inline, property)) { // If the only child is a Run, set the value directly. // Otherwise there's no need to set the value. inline.SetValue(property, value); } } // Finds a Paragraph/BlockUIContainer/List element with ElementStart before or at the given pointer // Creates implicit paragraphs at potential paragraph positions if needed private static Block GetNextBlock(TextPointer pointer, TextPointer limit) { Block block = null; while (pointer != null && pointer.CompareTo(limit) <= 0) { if (pointer.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart) { block = pointer.Parent as Block; if (block is Paragraph || block is BlockUIContainer || block is List) { break; } } if (TextPointerBase.IsAtPotentialParagraphPosition(pointer)) { pointer = TextRangeEditTables.EnsureInsertionPosition(pointer); block = pointer.Paragraph; Invariant.Assert(block != null); break; } // Advance the scanning pointer pointer = pointer.GetNextContextPosition(LogicalDirection.Forward); } return block; } // Helper for SetParagraphProperty private static Thickness ComputeNewThicknessValue(Thickness currentThickness, Thickness newThickness, FlowDirection parentFlowDirection, FlowDirection flowDirection, PropertyValueAction propertyValueAction) { // Negative value for particular axis means "leave it unchanged" double topMargin = newThickness.Top < 0 ? currentThickness.Top : NewValue(currentThickness.Top, newThickness.Top, propertyValueAction); double bottomMargin = newThickness.Bottom < 0 ? currentThickness.Bottom : NewValue(currentThickness.Bottom, newThickness.Bottom, propertyValueAction); double leftMargin; double rightMargin; if (parentFlowDirection != flowDirection) { // In case of mismatching FlowDirection between parent and current, // we apply value.Left to currentValue.Right and vice versa. // The caller of the method must account for that and use Left/Right margins appropriately. leftMargin = newThickness.Right < 0 ? currentThickness.Left : NewValue(currentThickness.Left, newThickness.Right, propertyValueAction); rightMargin = newThickness.Left < 0 ? currentThickness.Right : NewValue(currentThickness.Right, newThickness.Left, propertyValueAction); } else { leftMargin = newThickness.Left < 0 ? currentThickness.Left : NewValue(currentThickness.Left, newThickness.Left, propertyValueAction); rightMargin = newThickness.Right < 0 ? currentThickness.Right : NewValue(currentThickness.Right, newThickness.Right, propertyValueAction); } return new Thickness(leftMargin, topMargin, rightMargin, bottomMargin); } // Helper for SetParagraphProperty, flips TextAligment values when FlowDirection is RTL. private static TextAlignment ComputeNewTextAlignmentValue(TextAlignment textAlignment, FlowDirection flowDirection) { if (textAlignment == TextAlignment.Left) { textAlignment = (flowDirection == FlowDirection.LeftToRight) ? TextAlignment.Left : TextAlignment.Right; } else if (textAlignment == TextAlignment.Right) { textAlignment = (flowDirection == FlowDirection.LeftToRight) ? TextAlignment.Right : TextAlignment.Left; } return textAlignment; } // Applies newValue to the currentValue according to a propertyValueAction - // increments or just sets it. private static double NewValue(double currentValue, double newValue, PropertyValueAction propertyValueAction) { if (double.IsNaN(newValue)) { return newValue; } Invariant.Assert(newValue >= 0); if (double.IsNaN(currentValue)) { currentValue = 0.0; } newValue = propertyValueAction == PropertyValueAction.IncreaseByAbsoluteValue ? currentValue + newValue : propertyValueAction == PropertyValueAction.DecreaseByAbsoluteValue ? currentValue - newValue : propertyValueAction == PropertyValueAction.IncreaseByPercentageValue ? currentValue * (1.0 + newValue / 100) : propertyValueAction == PropertyValueAction.DecreaseByPercentageValue ? currentValue * (1.0 - newValue / 100) : newValue; if (newValue < 0.0) { newValue = 0.0; } return newValue; } // Translates TextAlignment value into corresponding HorizontalAlignment value. // Used in applying Paragraph.TextAlignmentProperty to BlockUIContainer elements. internal static HorizontalAlignment GetHorizontalAlignmentFromTextAlignment(TextAlignment textAlignment) { HorizontalAlignment horizontalAlignment; switch (textAlignment) { default: case TextAlignment.Left: horizontalAlignment = HorizontalAlignment.Left; break; case TextAlignment.Center: horizontalAlignment = HorizontalAlignment.Center; break; case TextAlignment.Right: horizontalAlignment = HorizontalAlignment.Right; break; case TextAlignment.Justify: horizontalAlignment = HorizontalAlignment.Stretch; break; } return horizontalAlignment; } // Translates HorizontalAlignment value into corresponding TextAlignment value. internal static TextAlignment GetTextAlignmentFromHorizontalAlignment(HorizontalAlignment horizontalAlignment) { TextAlignment textAlignment; switch (horizontalAlignment) { case HorizontalAlignment.Left: textAlignment = TextAlignment.Left; break; case HorizontalAlignment.Center: textAlignment = TextAlignment.Center; break; case HorizontalAlignment.Right: textAlignment = TextAlignment.Right; break; default: case HorizontalAlignment.Stretch: textAlignment = TextAlignment.Justify; break; } return textAlignment; } // Helper to set property value on element. private static void SetPropertyValue(TextElement element, DependencyProperty property, object currentValue, object newValue) { if (!TextSchema.ValuesAreEqual(newValue, currentValue)) { // first clear and see if it will do element.ClearValue(property); // if still need it, set it if (!TextSchema.ValuesAreEqual(newValue, element.GetValue(property))) { element.SetValue(property, newValue); } } } // Helper that swaps the left and right margins of a block element. private static void SwapBlockLeftAndRightMargins(Block block) { object value = block.GetValue(Block.MarginProperty); if (value is Thickness) { if (Paragraph.IsMarginAuto((Thickness)value)) { // Nothing to do for auto thickess } else { // Swap left and right values object newValue = new Thickness( /*left*/((Thickness)value).Right, /*top:*/((Thickness)value).Top, /*right:*/((Thickness)value).Left, /*bottom:*/((Thickness)value).Bottom); SetPropertyValue(block, Block.MarginProperty, value, newValue); } } } // Returns a pointer of a text range adjusted so it does not affect // the paragraph following the selection. internal static ITextPointer GetAdjustedRangeEnd(ITextPointer rangeStart, ITextPointer rangeEnd) { if (rangeStart.CompareTo(rangeEnd) < 0 && rangeEnd.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart) { rangeEnd = rangeEnd.GetNextInsertionPosition(LogicalDirection.Backward); if (rangeEnd == null) { rangeEnd = rangeStart; // Recover position for container start case - we never return null from this method. } } else if (TextPointerBase.IsAfterLastParagraph(rangeEnd)) { rangeEnd = rangeEnd.GetInsertionPosition(LogicalDirection.Backward); } return rangeEnd; } // Merges Spans or Runs with equal FlowDirection that border at a given position. internal static void MergeFlowDirection(TextPointer position) { TextPointerContext backwardContext = position.GetPointerContext(LogicalDirection.Backward); TextPointerContext forwardContext = position.GetPointerContext(LogicalDirection.Forward); if (!(backwardContext == TextPointerContext.ElementStart || backwardContext == TextPointerContext.ElementEnd) && !(forwardContext == TextPointerContext.ElementStart || forwardContext == TextPointerContext.ElementEnd)) { // Early out if position is not at an Inline border. return; } // Find the common ancestor of the two adjacent content runs. while (position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && TextSchema.IsMergeableInline(position.Parent.GetType())) { position = ((Inline)position.Parent).ElementStart; } while (position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd && TextSchema.IsMergeableInline(position.Parent.GetType())) { position = ((Inline)position.Parent).ElementEnd; } TextElement commonAncestor = position.Parent as TextElement; if (!(commonAncestor is Span || commonAncestor is Paragraph)) { // Don't try to merge across Block boundaries. return; } // Find the previous content. TextPointer previousPosition = position.CreatePointer(); while (previousPosition.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementEnd && TextSchema.IsMergeableInline(previousPosition.GetAdjacentElement(LogicalDirection.Backward).GetType())) { previousPosition = ((Inline)previousPosition.GetAdjacentElement(LogicalDirection.Backward)).ContentEnd; } Run previousRun = previousPosition.Parent as Run; // Find the next content. TextPointer nextPosition = position.CreatePointer(); while (nextPosition.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementStart && TextSchema.IsMergeableInline(nextPosition.GetAdjacentElement(LogicalDirection.Forward).GetType())) { nextPosition = ((Inline)nextPosition.GetAdjacentElement(LogicalDirection.Forward)).ContentStart; } Run nextRun = nextPosition.Parent as Run; if (previousRun == null || previousRun.IsEmpty || nextRun == null || nextRun.IsEmpty) { // No text to make the merge meaningful. return; } FlowDirection midpointFlowDirection = (FlowDirection)commonAncestor.GetValue(FrameworkElement.FlowDirectionProperty); FlowDirection previousFlowDirection = (FlowDirection)previousRun.GetValue(FrameworkElement.FlowDirectionProperty); FlowDirection nextFlowDirection = (FlowDirection)nextRun.GetValue(FrameworkElement.FlowDirectionProperty); // If the previous and next content have the same FlowDirection, but their // common ancestor differs, we want to merge them. if (previousFlowDirection == nextFlowDirection && previousFlowDirection != midpointFlowDirection) { // Expand the context out to include any scoping Spans with local FlowDirection. Inline scopingPreviousInline = GetScopingFlowDirectionInline(previousRun); Inline scopingNextInline = GetScopingFlowDirectionInline(nextRun); // Set a single FlowDirection Span over the whole lot of it. SetStructuralInlineProperty(scopingPreviousInline.ElementStart, scopingNextInline.ElementEnd, FrameworkElement.FlowDirectionProperty, previousFlowDirection); } } // Returns false if calling ApplyStructuralInlineProperty will throw an InvalidOperationException with the // same input parameters. // // In practice, this method returns false when the property apply would require that we split a // non-mergeable Inline such as Hyperlink. internal static bool CanApplyStructuralInlineProperty(TextPointer start, TextPointer end) { return ValidateApplyStructuralInlineProperty(start, end, TextPointer.GetCommonAncestor(start, end), null); } // ..................................................................... // // Paragraph Editing Commands // // ..................................................................... ////// Increments/decrements paragraph leading maring property. /// For LeftToRight paragraphs a leading maring is the left marinng, /// for RightToLeft paragraphs it is the right maring. /// /// /// /// /// Must be one of IncreaseValue or DecreaseValue. /// internal static void IncrementParagraphLeadingMargin(TextRange range, double increment, PropertyValueAction propertyValueAction) { Invariant.Assert(increment >= 0); Invariant.Assert(propertyValueAction != PropertyValueAction.SetValue); if (increment == 0) { // Nothing to do. Just return. return; } // Note that SetParagraphProperty method will swap Left and Right margins for RightToLeft paragraphs. // Note that -1 values for Thickness axis means leaving its value as is. Thickness thickness = new Thickness(increment, -1, -1, -1); // Apply paragraph margin property TextRangeEdit.SetParagraphProperty(range.Start, range.End, Block.MarginProperty, thickness, propertyValueAction); } ////// Deletes a content covered by two positions assuming that /// the content crosses only inline boundaries (if at all) - /// no Paragraph or any other Block or structural elements are /// supposed to be crossed (including Floaters and Figures). /// /// /// internal static void DeleteInlineContent(ITextPointer start, ITextPointer end) { DeleteParagraphContent(start, end); } ////// Deletes a content covered by two positions assuming that /// the content crosses only paragraph-mergeable boundaries (if at all) - /// Paragraphs, Sections, Lists, ListItems, but not harder structural /// elements like Tables, TableCells, TableRows, Floaters, Figures. /// /// /// Position indicating a beginning of deleted content. /// /// /// Position indicating an end of deleted content. /// internal static void DeleteParagraphContent(ITextPointer start, ITextPointer end) { // Parameters validation Invariant.Assert(start != null, "null check: start"); Invariant.Assert(end != null, "null check: end"); Invariant.Assert(start.CompareTo(end) <= 0, "expecting: start <= end"); if (!(start is TextPointer)) { // Abstract text container. We can only use basic abstract functionality here: start.DeleteContentToPosition(end); return; } TextPointer startPosition = (TextPointer)start; TextPointer endPosition = (TextPointer)end; // Delete all equi-scoped content in the given range DeleteEquiScopedContent(startPosition, endPosition); // delete content runs from start to root DeleteEquiScopedContent(endPosition, startPosition); // delete contentruns from end to root // Merge crossed elements if (startPosition.CompareTo(endPosition) < 0) { if (TextPointerBase.IsAfterLastParagraph(endPosition)) { // This means that end position is after the last paragraph of a text container. // When the last paragraph is empty (and selection crosses its end boundary) // we need to delete it. // When last paragraph is not empty, we have to leave it as is. while (startPosition.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && startPosition.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd) { // TextElement parent = (TextElement)startPosition.Parent; if (parent is Inline || TextSchema.AllowsParagraphMerging(parent.GetType())) { parent.RepositionWithContent(null); } else { break; } } } else { Block firstParagraphOrBlockUIContainer = startPosition.ParagraphOrBlockUIContainer; Block secondParagraphOrBlockUIContainer = endPosition.ParagraphOrBlockUIContainer; // If startPosition and/or endPosition is parented by an empty ListItem, create an implicit paragraph in it. // This will enable the following code to merge paragraphs in list items. if (firstParagraphOrBlockUIContainer == null && TextPointerBase.IsInEmptyListItem(startPosition)) { startPosition = TextRangeEditTables.EnsureInsertionPosition(startPosition); firstParagraphOrBlockUIContainer = startPosition.Paragraph; Invariant.Assert(firstParagraphOrBlockUIContainer != null, "EnsureInsertionPosition must create a paragraph inside list item - 1"); } if (secondParagraphOrBlockUIContainer == null && TextPointerBase.IsInEmptyListItem(endPosition)) { endPosition = TextRangeEditTables.EnsureInsertionPosition(endPosition); secondParagraphOrBlockUIContainer = endPosition.Paragraph; Invariant.Assert(secondParagraphOrBlockUIContainer != null, "EnsureInsertionPosition must create a paragraph inside list item - 2"); } if (firstParagraphOrBlockUIContainer != null && secondParagraphOrBlockUIContainer != null) { TextRangeEditLists.MergeParagraphs(firstParagraphOrBlockUIContainer, secondParagraphOrBlockUIContainer); } else { // When crossing BlockUIContainer boundaries we need to clear // any empty BlockUIContainers and empty adjacent paragraphs MergeEmptyParagraphsAndBlockUIContainers(startPosition, endPosition); } } } // Remove empty formatting elements MergeFormattingInlines(startPosition); MergeFormattingInlines(endPosition); // Check for remaining empty BlockUICOntainer or empty Hyperlink elements if (startPosition.Parent is BlockUIContainer && ((BlockUIContainer)startPosition.Parent).IsEmpty) { ((BlockUIContainer)startPosition.Parent).Reposition(null, null); } else if (startPosition.Parent is Hyperlink && ((Hyperlink)startPosition.Parent).IsEmpty) { ((Hyperlink)startPosition.Parent).Reposition(null, null); // After deleting an empty hyperlink, we might have inlines to merge. MergeFormattingInlines(startPosition); } // } // Helper for DeleteParagraphContent // Takes 2 positions possibly parented by paragraph or BlockUIContainer // and deletes them if they are empty . private static void MergeEmptyParagraphsAndBlockUIContainers(TextPointer startPosition, TextPointer endPosition) { Block first = startPosition.ParagraphOrBlockUIContainer; Block second = endPosition.ParagraphOrBlockUIContainer; if (first is BlockUIContainer) { if (first.IsEmpty) { first.Reposition(null, null); return; } else if (second is Paragraph && Paragraph.HasNoTextContent((Paragraph) second)) { second.RepositionWithContent(null); return; } } if (second is BlockUIContainer) { if (second.IsEmpty) { second.Reposition(null, null); return; } else if (second is Paragraph && Paragraph.HasNoTextContent((Paragraph) first)) { first.RepositionWithContent(null); return; } } } ////// Deletes all equi-scoped segments of content from start TextPointer /// up to fragment root. Thus clears one half of a fragment. /// The other half remains untouched. /// All elements whose boundaries are crossed by this range /// remain in the tree (except for emptied formatting elements). /// /// /// A position from which content clearinng starts. /// All content segments between this position and a fragment /// root will be deleted. /// /// /// A position indicating the other boundary of a fragment. /// This position is used for fragment root identification. /// private static void DeleteEquiScopedContent(TextPointer start, TextPointer end) { // Validate parameters Invariant.Assert(start != null, "null check: start"); Invariant.Assert(end != null, "null check: end"); if (start.CompareTo(end) == 0) { return; } if (start.Parent == end.Parent) { DeleteContentBetweenPositions(start, end); return; } // Identify directional parameters LogicalDirection direction; LogicalDirection oppositeDirection; TextPointerContext enterScopeSymbol; TextPointerContext leaveScopeSymbol; ElementEdge edgeBeforeElement; ElementEdge edgeAfterElement; if (start.CompareTo(end) < 0) { direction = LogicalDirection.Forward; oppositeDirection = LogicalDirection.Backward; enterScopeSymbol = TextPointerContext.ElementStart; leaveScopeSymbol = TextPointerContext.ElementEnd; edgeBeforeElement = ElementEdge.BeforeStart; edgeAfterElement = ElementEdge.AfterEnd; } else { direction = LogicalDirection.Backward; oppositeDirection = LogicalDirection.Forward; enterScopeSymbol = TextPointerContext.ElementEnd; leaveScopeSymbol = TextPointerContext.ElementStart; edgeBeforeElement = ElementEdge.AfterEnd; edgeAfterElement = ElementEdge.BeforeStart; } // previousPosition will store a location where nondeleted content starts TextPointer previousPosition = new TextPointer(start); // nextPosition runs toward other end until level change - // so that we could delete all content from previousPosition // to nextPosition at once. TextPointer nextPosition = new TextPointer(start); // Run nextPosition forward until the very end of affected range while (nextPosition.CompareTo(end) != 0) { Invariant.Assert(direction == LogicalDirection.Forward && nextPosition.CompareTo(end) < 0 || direction == LogicalDirection.Backward && nextPosition.CompareTo(end) > 0, "Inappropriate position ordering"); Invariant.Assert(previousPosition.Parent == nextPosition.Parent, "inconsistent position Parents: previous and next"); TextPointerContext pointerContext = nextPosition.GetPointerContext(direction); if (pointerContext == TextPointerContext.Text || pointerContext == TextPointerContext.EmbeddedElement) { // Add this run to a collection of equi-scoped content nextPosition.MoveToNextContextPosition(direction); // Check if we went too far and return a little to end if necessary if (direction == LogicalDirection.Forward && nextPosition.CompareTo(end) > 0 || direction == LogicalDirection.Backward && nextPosition.CompareTo(end) < 0) { Invariant.Assert(nextPosition.Parent == end.Parent, "inconsistent poaition Parents: next and end"); nextPosition.MoveToPosition(end); break; } } else if (pointerContext == enterScopeSymbol) { // Jump over the element and continue collecting equi-scoped content nextPosition.MoveToNextContextPosition(direction); ((ITextPointer)nextPosition).MoveToElementEdge(edgeAfterElement); // If our range crosses the element then we stop before its opening tag if (direction == LogicalDirection.Forward && nextPosition.CompareTo(end) >= 0 || direction == LogicalDirection.Backward && nextPosition.CompareTo(end) <= 0) { nextPosition.MoveToNextContextPosition(oppositeDirection); ((ITextPointer)nextPosition).MoveToElementEdge(edgeBeforeElement); break; } } else if (pointerContext == leaveScopeSymbol) { // Delete preceding content and continue on outer level DeleteContentBetweenPositions(previousPosition, nextPosition); if (!ExtractEmptyFormattingElements(previousPosition)) { // Continue on outer level Invariant.Assert(nextPosition.GetPointerContext(direction) == leaveScopeSymbol, "Unexpected context of nextPosition"); nextPosition.MoveToNextContextPosition(direction); } previousPosition.MoveToPosition(nextPosition); } else { Invariant.Assert(false, "Not expecting None context here"); Invariant.Assert(pointerContext == TextPointerContext.None, "Unknown pointer context"); break; } } Invariant.Assert(previousPosition.Parent == nextPosition.Parent, "inconsistent Parents: previousPosition, nextPosition"); DeleteContentBetweenPositions(previousPosition, nextPosition); } ////// Helper for TextContainer.DeleteContent allowing arbitrary /// order of positions and doinng nothing in case of empty range. /// Removes remaining empty formatting elements - if they not inside empty blocks. /// /// /// One of content boundary positions. May precede or follow the TextPointer two. /// Must belong to the same scope as TextPointer two. /// /// /// Another content boundary position. May precede or follow the TextPointer one. /// Must belong to the same scope as TextPointer one. /// ////// true if surrounding formatting elements have beed deleted as a side effect. /// private static bool DeleteContentBetweenPositions(TextPointer one, TextPointer two) { Invariant.Assert(one.Parent == two.Parent, "inconsistent Parents: one and two"); if (one.CompareTo(two) < 0) { one.TextContainer.DeleteContentInternal(one, two); } else if (one.CompareTo(two) > 0) { two.TextContainer.DeleteContentInternal(two, one); } Invariant.Assert(one.CompareTo(two) == 0, "Positions one and two must be equal now"); return false; } #endregion Paragraph Editing #endregion Internal Methods // -------------------------------------------------------------------- // // Private Methods // // ------------------------------------------------------------------- #region Private Methods private static TextPointer SplitFormattingElements(TextPointer splitPosition, bool keepEmptyFormatting, TextElement limitingAncestor) { return SplitFormattingElements(splitPosition, keepEmptyFormatting, /*preserveStructuralFormatting*/false, limitingAncestor); } ////// Splits all inline element walking up to specified limitingAncestor. /// limitingAncestor remains unsplit. /// /// /// Position at which splitting happens. After the operation the position /// is between split elements - scoped by limitingElement (if it is not frozen). /// /// /// Flag to indicate whether split operation should create empty formatting tags. /// /// /// If true, ensures that structural properties are preserved on elements. Runs will be split /// after creating a wrapping Span preserving the original structural property value, otherwise /// splitting will halt when a non-Run element has a local structural property (as if a limiting /// ancestor or non-mergeable inline had been encountered). /// /// /// If null, this has no impact on split operation. /// Otherwise, this method ensures that this ancestor boundary is not crossed while splitting. /// ////// TextPointer positioned in between two elements. /// It may be the same instance as splitPosition parameter /// (in case if it was not frozen), or some new instance of TextPointer. /// private static TextPointer SplitFormattingElements(TextPointer splitPosition, bool keepEmptyFormatting, bool preserveStructuralFormatting, TextElement limitingAncestor) { if (preserveStructuralFormatting) { Run run = splitPosition.Parent as Run; if (run != null && run != limitingAncestor && HasLocalInheritableStructuralPropertyValue(run)) { // This Run has a structural property set on it (eg, FlowDirection) which cannot simply be split // (two adjacent Runs with the same FlowDirection will render differently than a single Run with // the same value, when the parent FlowDirection property differs). // So create a wrapping Span which will survive in the loop below. Span span = new Span(run.ElementStart, run.ElementEnd); TransferStructuralProperties(run, span); } } // Splitting loop: cutting a parent element until we reach the non-inline, // never crossing ancestor boundary. while (splitPosition.Parent != null && TextSchema.IsMergeableInline(splitPosition.Parent.GetType()) && splitPosition.Parent != limitingAncestor && (!preserveStructuralFormatting || !HasLocalInheritableStructuralPropertyValue((Inline)splitPosition.Parent))) { splitPosition = SplitFormattingElement(splitPosition, keepEmptyFormatting); } return splitPosition; } // Copies all structural properties from source (clearing the property) to destination. private static void TransferStructuralProperties(Inline source, Inline destination) { bool sourceIsChild = (source.Parent == destination); for (int i = 0; i < TextSchema.StructuralCharacterProperties.Length; i++) { DependencyProperty property = TextSchema.StructuralCharacterProperties[i]; if ((sourceIsChild && HasLocalInheritableStructuralPropertyValue(source)) || (!sourceIsChild && HasLocalStructuralPropertyValue(source))) { object value = source.GetValue(property); source.ClearValue(property); destination.SetValue(property, value); } } } // Returns true if an Inline has one or more non-readonly local property values. private static bool HasWriteableLocalPropertyValues(Inline inline) { LocalValueEnumerator enumerator = inline.GetLocalValueEnumerator(); bool hasLocalValues = false; while (!hasLocalValues && enumerator.MoveNext()) { hasLocalValues = !enumerator.Current.Property.ReadOnly; } return hasLocalValues; } // Returns true if an inline has one or more structural local property values. private static bool HasLocalInheritableStructuralPropertyValue(Inline inline) { int i; for (i = 0; i < TextSchema.StructuralCharacterProperties.Length; i++) { DependencyProperty inheritableProperty = TextSchema.StructuralCharacterProperties[i]; if (!TextSchema.ValuesAreEqual(inline.GetValue(inheritableProperty), inline.Parent.GetValue(inheritableProperty))) break; } return (i < TextSchema.StructuralCharacterProperties.Length); } // Returns true if an inline has one or more structural local property values. private static bool HasLocalStructuralPropertyValue(Inline inline) { int i; for (i = 0; i < TextSchema.StructuralCharacterProperties.Length; i++) { DependencyProperty inheritableProperty = TextSchema.StructuralCharacterProperties[i]; if (HasLocalPropertyValue(inline, inheritableProperty)) break; } return (i < TextSchema.StructuralCharacterProperties.Length); } // Returns true if an inline has a local property value with higher precedence than inheritance. private static bool HasLocalPropertyValue(Inline inline, DependencyProperty property) { bool hasModifiers; BaseValueSourceInternal source = inline.GetValueSource(property, null, out hasModifiers); return (source != BaseValueSourceInternal.Unknown && source != BaseValueSourceInternal.Default && source != BaseValueSourceInternal.Inherited); } // Helper for MergeFlowDirection. Returns a greatest scoping Inline of a Run // with matching FlowDirection. The caller guarantees that a scoping Span // has differing FlowDirection. private static Inline GetScopingFlowDirectionInline(Run run) { FlowDirection flowDirection = run.FlowDirection; Inline inline = run; while ((FlowDirection)inline.Parent.GetValue(FrameworkElement.FlowDirectionProperty) == flowDirection) { inline = (Span)inline.Parent; } return inline; } // Helper to set non-structural Inline property to a range between start and end positions. private static void SetNonStructuralInlineProperty(TextPointer start, TextPointer end, DependencyProperty formattingProperty, object value, PropertyValueAction propertyValueAction) { // Split formatting elements at range boundaries start = SplitFormattingElements(start, /*keepEmptyFormatting:*/false, /*preserveStructuralFormatting*/true, /*limitingAncestor*/null); end = SplitFormattingElements(end, /*keepEmptyFormatting:*/false, /*preserveStructuralFormatting*/true, /*limitingAncestor*/null); Run run = TextRangeEdit.GetNextRun(start, end); while (run != null) { object currentValue = run.GetValue(formattingProperty); object newValue = value; if (propertyValueAction != PropertyValueAction.SetValue) { Invariant.Assert(formattingProperty == TextElement.FontSizeProperty, "Only FontSize can be incremented/decremented among character properties"); newValue = GetNewFontSizeValue((double)currentValue, (double)value, propertyValueAction); } // Set new property value SetPropertyValue(run, formattingProperty, currentValue, newValue); // Remember a position after the current run for the following processing. // Normalize forward since Run.ElementEnd has backward gravity. TextPointer nextRunPosition = run.ElementEnd.GetPositionAtOffset(0, LogicalDirection.Forward); if (TextPointerBase.IsAtPotentialRunPosition(run)) { // If current run was an implicit run, we move to the next context position after its element end. // This is safe because by definition of IsAtPotentialRunPosition predicate, // our current run can never have an adjacent run element or // another adjacent potential run position. nextRunPosition = nextRunPosition.GetNextContextPosition(LogicalDirection.Forward); } // Merge this run with the previous one. // Note that this can affect text structure even after this run. MergeFormattingInlines(run.ContentStart); // Find the next Run to process run = TextRangeEdit.GetNextRun(nextRunPosition, end); } MergeFormattingInlines(end); } // Helper to calculate new value of Run.FontSize property when PropertyValueAction is increment/decrement. private static double GetNewFontSizeValue(double currentValue, double value, PropertyValueAction propertyValueAction) { double newValue = value; // Calculate the new value as increment/decrement from the current value if (propertyValueAction == PropertyValueAction.IncreaseByAbsoluteValue) { newValue = currentValue + value; } else if (propertyValueAction == PropertyValueAction.DecreaseByAbsoluteValue) { newValue = currentValue - value; } // Check limiting boundaries if (newValue < TextEditorCharacters.OneFontPoint) { newValue = TextEditorCharacters.OneFontPoint; } else if (newValue > TextEditorCharacters.MaxFontPoint) { newValue = TextEditorCharacters.MaxFontPoint; } return newValue; } // Helper to set a structural Inline property to a range between start and end positions. private static void SetStructuralInlineProperty(TextPointer start, TextPointer end, DependencyProperty formattingProperty, object value) { DependencyObject commonAncestor = TextPointer.GetCommonAncestor(start, end); ValidateApplyStructuralInlineProperty(start, end, commonAncestor, formattingProperty); if (commonAncestor is Run) { ApplyStructuralInlinePropertyAcrossRun(start, end, (Run)commonAncestor, formattingProperty, value); } else if ((commonAncestor is Inline && !(commonAncestor is AnchoredBlock)) || commonAncestor is Paragraph) { // Even though we don't test for it explicitly, we // should never see InlineUIContainers here because start/end // are always normalized and the inner edges of InlineUIContainer // are not insertion positions. Invariant.Assert(!(commonAncestor is InlineUIContainer)); ApplyStructuralInlinePropertyAcrossInline(start, end, (TextElement)commonAncestor, formattingProperty, value); } else { ApplyStructuralInlinePropertyAcrossParagraphs(start, end, formattingProperty, value); } } private static void FixupStructuralPropertyEnvironment(Inline inline, DependencyProperty property) { // Clear property on parent Spans. ClearParentStructuralPropertyValue(inline, property); // Flatten property on previous Inlines. for (Inline searchInline = inline; searchInline != null; searchInline = searchInline.Parent as Span) { Inline previousSibling = (Inline)searchInline.PreviousElement; if (previousSibling != null) { FlattenStructuralProperties(previousSibling); break; } } // Flatten property on following Inlines. for (Inline searchInline = inline; searchInline != null; searchInline = searchInline.Parent as Span) { Inline nextSibling = (Inline)searchInline.NextElement; if (nextSibling != null) { FlattenStructuralProperties(nextSibling); break; } } } private static void FlattenStructuralProperties(Inline inline) { // Find the topmost Span covering this inline and only other direct ancestors. Span topmostSpan = inline as Span; Span parent = inline.Parent as Span; while (parent != null && parent.Inlines.FirstInline == parent.Inlines.LastInline) { topmostSpan = parent; parent = parent.Parent as Span; } // Push structural properties downward. while (topmostSpan != null && topmostSpan.Inlines.FirstInline == topmostSpan.Inlines.LastInline) { Inline child = (Inline)topmostSpan.Inlines.FirstInline; TransferStructuralProperties(topmostSpan, child); // If there are no more local values on the parent, remove it. if (TextSchema.IsMergeableInline(topmostSpan.GetType()) && TextSchema.IsKnownType(topmostSpan.GetType()) && !HasWriteableLocalPropertyValues(topmostSpan)) { topmostSpan.Reposition(null, null); } topmostSpan = child as Span; } } private static void ClearParentStructuralPropertyValue(Inline child, DependencyProperty property) { // Find the most distant ancestor with a local property value. Span conflictingParent = null; for (Span parent = child.Parent as Span; parent != null && TextSchema.IsMergeableInline(parent.GetType()); parent = parent.Parent as Span) { if (HasLocalPropertyValue(parent, property)) { conflictingParent = parent; } } // Split down from conflictingParent, clearing property values along the way. if (conflictingParent != null) { TextElement limit = (TextElement)conflictingParent.Parent; SplitFormattingElements(child.ElementStart, /*keepEmptyFormatting*/false, limit); TextPointer end = SplitFormattingElements(child.ElementEnd, /*keepEmptyFormatting*/false, limit); Span parent = (Span)end.GetAdjacentElement(LogicalDirection.Backward); while (parent != null && parent != child) { parent.ClearValue(property); Span nextSpan = parent.Inlines.FirstInline as Span; // If there are no more local values on the parent, remove it. if (!HasWriteableLocalPropertyValues(parent)) { // parent.Reposition(null, null); } parent = nextSpan; } } } // Finds a Run element with ElementStart at or after the given pointer // Creates Runs at potential run positions if encounters some. private static Run GetNextRun(TextPointer pointer, TextPointer limit) { Run run = null; while (pointer != null && pointer.CompareTo(limit) < 0) { if (pointer.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementStart && (run = pointer.GetAdjacentElement(LogicalDirection.Forward) as Run) != null) { break; } if (TextPointerBase.IsAtPotentialRunPosition(pointer)) { pointer = TextRangeEditTables.EnsureInsertionPosition(pointer); Invariant.Assert(pointer.Parent is Run); run = pointer.Parent as Run; break; } // Advance the scanning pointer pointer = pointer.GetNextContextPosition(LogicalDirection.Forward); } return run; } // Helper that walks Run and Span elements between start and end positions, // clearing value of passed formattingProperty on them. // private static void ClearPropertyValueFromSpansAndRuns(TextPointer start, TextPointer end, DependencyProperty formattingProperty) { // Normalize start position forward. start = start.GetPositionAtOffset(0, LogicalDirection.Forward); // Move to next context position before entering loop below, // since in the loop we look backward. start = start.GetNextContextPosition(LogicalDirection.Forward); while (start != null && start.CompareTo(end) < 0) { if (start.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && TextSchema.IsFormattingType(start.Parent.GetType())) // look for Run/Span elements { start.Parent.ClearValue(formattingProperty); // Remove unnecessary Spans around this position, delete empty formatting elements (if any) // and merge with adjacent inlines if they have identical set of formatting properties. MergeFormattingInlines(start); } start = start.GetNextContextPosition(LogicalDirection.Forward); } } private static void ApplyStructuralInlinePropertyAcrossRun(TextPointer start, TextPointer end, Run run, DependencyProperty formattingProperty, object value) { if (start.CompareTo(end) == 0) { // When the range is empty we should ignore the command, except // for the case of empty Run which can be encountered in empty paragraphs if (run.IsEmpty) { run.SetValue(formattingProperty, value); } } else { // Split elements at start and end boundaries. start = SplitFormattingElements(start, /*keepEmptyFormatting:*/false, /*limitingAncestor*/run.Parent as TextElement); end = SplitFormattingElements(end, /*keepEmptyFormatting:*/false, /*limitingAncestor*/run.Parent as TextElement); run = (Run)start.GetAdjacentElement(LogicalDirection.Forward); run.SetValue(formattingProperty, value); } // Clear property value from all ancestors of this Run. FixupStructuralPropertyEnvironment(run, formattingProperty); } private static void ApplyStructuralInlinePropertyAcrossInline(TextPointer start, TextPointer end, TextElement commonAncestor, DependencyProperty formattingProperty, object value) { start = SplitFormattingElements(start, /*keepEmptyFormatting:*/false, commonAncestor); end = SplitFormattingElements(end, /*keepEmptyFormatting:*/false, commonAncestor); DependencyObject forwardElement = start.GetAdjacentElement(LogicalDirection.Forward); DependencyObject backwardElement = end.GetAdjacentElement(LogicalDirection.Backward); if (forwardElement == backwardElement && (forwardElement is Run || forwardElement is Span)) { // After splitting we have exactly one Run or Span between start and end. Use it for setting the property. Inline inline = (Inline)start.GetAdjacentElement(LogicalDirection.Forward); // Set the property to existing element. inline.SetValue(formattingProperty, value); // Clear property value from all ancestors of this inline. FixupStructuralPropertyEnvironment(inline, formattingProperty); if (forwardElement is Span) { // Clear property value from all Span and Run children of this span. ClearPropertyValueFromSpansAndRuns(inline.ContentStart, inline.ContentEnd, formattingProperty); } } else { Span span; if (commonAncestor is Span && start.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && end.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd && start.GetAdjacentElement(LogicalDirection.Backward) == commonAncestor) { // Special case when start and end are at parent Span boundaries. // Don't need to create a new Span in this case. span = (Span)commonAncestor; } else { // Create a new span from start to end. span = new Span(); span.Reposition(start, end); } // Set property on the span. span.SetValue(formattingProperty, value); // Clear property value from all ancestors of this span. FixupStructuralPropertyEnvironment(span, formattingProperty); // Clear property value from all Span and Run children of this span. ClearPropertyValueFromSpansAndRuns(span.ContentStart, span.ContentEnd, formattingProperty); } } // Helper that walks paragraphs between start and end positions, applying passed formattingProperty value on them. private static void ApplyStructuralInlinePropertyAcrossParagraphs(TextPointer start, TextPointer end, DependencyProperty formattingProperty, object value) { // We assume to call this method only for paragraph crossing case Invariant.Assert(start.Paragraph != null); Invariant.Assert(start.Paragraph.ContentEnd.CompareTo(end) < 0); // Apply to first Paragraph SetStructuralInlineProperty(start, start.Paragraph.ContentEnd, formattingProperty, value); start = start.Paragraph.ElementEnd; // Apply to last paragraph if (end.Paragraph != null) { SetStructuralInlineProperty(end.Paragraph.ContentStart, end, formattingProperty, value); end = end.Paragraph.ElementStart; } // Now, loop through paragraphs between start and end positions while (start != null && start.CompareTo(end) < 0) { if (start.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && start.Parent is Paragraph) { Paragraph paragraph = (Paragraph)start.Parent; // Apply property to paragraph just found. SetStructuralInlineProperty(paragraph.ContentStart, paragraph.ContentEnd, formattingProperty, value); // Jump to Paragraph end to skip Inline formatting tags. start = paragraph.ElementEnd; } start = start.GetNextContextPosition(LogicalDirection.Forward); } } // Returns false if calling ApplyStructuralInlineProperty will throw an InvalidOperationException with the // same input parameters. // // If property != null, this method will throw an InvalidOperation exception instead of returning false. private static bool ValidateApplyStructuralInlineProperty(TextPointer start, TextPointer end, DependencyObject commonAncestor, DependencyProperty property) { if (!(commonAncestor is Inline)) { return true; } Inline nonMergeableAncestor = null; Inline parent; // Find the first non-mergeable Inline scoping start. for (parent = (Inline)start.Parent; parent != commonAncestor; parent = (Inline)parent.Parent) { if (!TextSchema.IsMergeableInline(parent.GetType())) { nonMergeableAncestor = parent; commonAncestor = parent; break; } } // Try to reach the start non-mergeable or original commonAncestor from end. for (parent = (Inline)end.Parent; parent != commonAncestor; parent = (Inline)parent.Parent) { if (!TextSchema.IsMergeableInline(parent.GetType())) { nonMergeableAncestor = parent; break; } } if (property != null && parent != commonAncestor) { throw new InvalidOperationException(SR.Get(SRID.TextRangeEdit_InvalidStructuralPropertyApply, property, nonMergeableAncestor)); } return (parent == commonAncestor); } #endregion Private Methods } } // File provided for Reference Use Only by Microsoft Corporation (c) 2007. // Copyright (c) Microsoft Corporation. All rights reserved. //---------------------------------------------------------------------------- // // File: TextRangeEdit.cs // // Copyright (C) Microsoft Corporation. All rights reserved. // // Description: Static internal class providing a set of // helpoer methods for text editing operations // //--------------------------------------------------------------------------- namespace System.Windows.Documents { using System; using MS.Internal; using System.Windows.Controls; ////// The TextRange class represents a pair of TextPositions, with many /// rich text editing operations exposed. /// internal static class TextRangeEdit { // ------------------------------------------------------------------- // // Internal Methods // // ------------------------------------------------------------------- #region Internal Methods internal static TextElement InsertElementClone(TextPointer start, TextPointer end, TextElement element) { TextElement newElement = (TextElement)Activator.CreateInstance(element.GetType()); // Copy properties to the newElement newElement.TextContainer.SetValues(newElement.ContentStart, element.GetLocalValueEnumerator()); newElement.Reposition(start, end); return newElement; } // .................................................................... // // Character Formatting // // .................................................................... #region Character Formatting ////// internal static TextPointer SplitFormattingElements(TextPointer splitPosition, bool keepEmptyFormatting) { return SplitFormattingElements(splitPosition, keepEmptyFormatting, /*limitingAncestor*/null); } internal static TextPointer SplitFormattingElement(TextPointer splitPosition, bool keepEmptyFormatting) { Invariant.Assert(splitPosition.Parent != null && TextSchema.IsMergeableInline(splitPosition.Parent.GetType())); Inline inline = (Inline)splitPosition.Parent; // Create a movable copy of a splitPosition if (splitPosition.IsFrozen) { splitPosition = new TextPointer(splitPosition); } if (!keepEmptyFormatting && splitPosition.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart) { // The first part of element is empty. We are allowed to remove empty formatting elements, // so we can simply move splitPotision outside of the element and we are done splitPosition.MoveToPosition(inline.ElementStart); } else if (!keepEmptyFormatting && splitPosition.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd) { // The second part of element is empty. We are allowed to remove empty formatting elements, // so we can simply move splitPotision outside of the element and we are done. splitPosition.MoveToPosition(inline.ElementEnd); } else { splitPosition = SplitElement(splitPosition); } return splitPosition; } // Compares a set of inheritable properties taken from two objects private static bool InheritablePropertiesAreEqual(Inline firstInline, Inline secondInline) { Invariant.Assert(firstInline != null, "null check: firstInline"); Invariant.Assert(secondInline != null, "null check: secondInline"); // Compare inheritable properties DependencyProperty[] inheritableProperties = TextSchema.GetInheritableProperties(typeof(Inline)); for (int i = 0; i < inheritableProperties.Length; i++) { DependencyProperty property = inheritableProperties[i]; if (TextSchema.IsStructuralCharacterProperty(property)) { if (firstInline.ReadLocalValue(property) != DependencyProperty.UnsetValue || secondInline.ReadLocalValue(property) != DependencyProperty.UnsetValue) { return false; } } else { if (!TextSchema.ValuesAreEqual(firstInline.GetValue(property), secondInline.GetValue(property))) { return false; } } } return true; } // Compares all character formatting properties for two elements. // Returns true if all known properties have equal values, false otherwise. // Note that only statically known character formatting properties // are taken into account. We intentionally ignore all other properties, // because TextEditor is not aware (in general) about their semantics, // and considers unsafe to duplicate them freely. // Ignorance means deletion, which is considered as safer approach. private static bool CharacterPropertiesAreEqual(Inline firstElement, Inline secondElement) { Invariant.Assert(firstElement != null, "null check: firstElement"); if (secondElement == null) { return false; } DependencyProperty[] noninheritableProperties = TextSchema.GetNoninheritableProperties(typeof(Span)); for (int i = 0; i < noninheritableProperties.Length; i++) { DependencyProperty property = noninheritableProperties[i]; if (!TextSchema.ValuesAreEqual(firstElement.GetValue(property), secondElement.GetValue(property))) { return false; } } if (!InheritablePropertiesAreEqual(firstElement, secondElement)) { return false; } return true; } ////// /// Checks if scoping element is empty formatting. /// It must be removed if not situated inside of empty block. /// /// /// TextPointer scoped by the allegedly empty formatting element(s). /// ////// true if at least one empty formatting element was extracted. /// private static bool ExtractEmptyFormattingElements(TextPointer position) { bool elementsWereExtracted = false; Inline inline = position.Parent as Inline; if (inline != null && inline.IsEmpty) { // Delete any empty non-formatting element. // We can get here if an IME deletes the UIElement from inside an InlineUIContainer. while (inline != null && inline.IsEmpty && !TextSchema.IsFormattingType(inline.GetType())) { inline.Reposition(null, null); elementsWereExtracted = true; inline = position.Parent as Inline; } // Start with removing empty Runs and Spans unconditionally. // If it is an empty non-derived Run or Span with no local properties on it - it's safe to delete it. // It does not have any formatting or any other meaning, while it can be implicitely // re-inserted when necessary. So remove it to minimize resulting xaml. while ( inline != null && inline.IsEmpty && (inline.GetType() == typeof(Run) || inline.GetType() == typeof(Span)) && !HasWriteableLocalPropertyValues(inline)) { inline.Reposition(null, null); elementsWereExtracted = true; inline = position.Parent as Inline; } // Continue deleting empty inlines that are neighbored by other formatting elements, // that make them inaccessible for caret position while (inline != null && inline.IsEmpty && ((inline.NextInline != null && TextSchema.IsFormattingType(inline.NextInline.GetType())) || (inline.PreviousInline != null && TextSchema.IsFormattingType(inline.PreviousInline.GetType())))) { inline.Reposition(null, null); elementsWereExtracted = true; inline = position.Parent as Inline; } } return elementsWereExtracted; } ////// Applies a property to a range between start and end positions. /// /// /// TextPointer identifying start of affected range. /// /// /// TextPointer identifying end of affected range. /// /// /// A dependency property whose value is supposed to applied to a range. /// /// /// A value for a property to apply. /// /// /// Specifies how to use the value - as absolute, as increment or a decrement. /// internal static void SetInlineProperty(TextPointer start, TextPointer end, DependencyProperty formattingProperty, object value, PropertyValueAction propertyValueAction) { // Check for corner case when we have siple text run with all properties set as requested. // This case is iportant optimization for Backspace-Type scenario, when Springload formatting applies for nothing for 50 properties if (start.CompareTo(end) >= 0 || propertyValueAction == PropertyValueAction.SetValue && start.Parent is Run && start.Parent == end.Parent && TextSchema.ValuesAreEqual(start.Parent.GetValue(formattingProperty), value)) { return; } // Remove unnecessary spans on range ends - to optimize resulting markup RemoveUnnecessarySpans(start); RemoveUnnecessarySpans(end); if (TextSchema.IsStructuralCharacterProperty(formattingProperty)) { SetStructuralInlineProperty(start, end, formattingProperty, value); } else { SetNonStructuralInlineProperty(start, end, formattingProperty, value, propertyValueAction); } } // Merges inline elements with equivalent formatting properties at a given position // Returns true if some changes happened at this position, false otherwise internal static bool MergeFormattingInlines(TextPointer position) { // Remove unnecessary Spans around this position RemoveUnnecessarySpans(position); // Delete empty formatting elements at this position (if any) ExtractEmptyFormattingElements(position); // Skip formatting tags towards potential merging position while (position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && TextSchema.IsMergeableInline(position.Parent.GetType())) { position = ((Inline)position.Parent).ElementStart; } while (position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd && TextSchema.IsMergeableInline(position.Parent.GetType())) { position = ((Inline)position.Parent).ElementEnd; } // Merge formatting Inlines at this position Inline firstInline, secondInline; bool merged = false; while ( position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementEnd && position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementStart && (firstInline = position.GetAdjacentElement(LogicalDirection.Backward) as Inline) != null && (secondInline = position.GetAdjacentElement(LogicalDirection.Forward) as Inline) != null) { if (TextSchema.IsFormattingType(firstInline.GetType()) && firstInline.TextRange.IsEmpty) { firstInline.RepositionWithContent(null); merged = true; } else if (TextSchema.IsFormattingType(secondInline.GetType()) && secondInline.TextRange.IsEmpty) { secondInline.RepositionWithContent(null); merged = true; } else if (TextSchema.IsKnownType(firstInline.GetType()) && TextSchema.IsKnownType(secondInline.GetType()) && (firstInline is Run && secondInline is Run || firstInline is Span && secondInline is Span) && TextSchema.IsMergeableInline(firstInline.GetType()) && TextSchema.IsMergeableInline(secondInline.GetType()) && CharacterPropertiesAreEqual(firstInline, secondInline)) { firstInline.Reposition(firstInline.ElementStart, secondInline.ElementEnd); secondInline.Reposition(null, null); merged = true; } else { break; } } // Now that Inlines have been merged we can try to optimize tree structure // by eliminating some unecessary wrapping Inlines if (merged) { RemoveUnnecessarySpans(position); } return merged; } // Inspects the tree up from a given position to find Span elements // wrapping exactly one other Span or Run - and removes them // after transferring all affected properties into inner element. private static void RemoveUnnecessarySpans(TextPointer position) { Inline inline = position.Parent as Inline; while (inline != null) { if (inline.Parent != null && TextSchema.IsMergeableInline(inline.Parent.GetType()) && TextSchema.IsKnownType(inline.Parent.GetType()) && inline.ElementStart.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && inline.ElementEnd.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd) { // Parent of this inline can be deleted. Let's delete it. Span parentSpan = (Span)inline.Parent; if (parentSpan.Parent == null) { break; } // We are going to delete a parent of this inline as it wraps only one child. // Before deleting we need to transfer all properties that are affected by that parent inline. // Transfer inheritable properties DependencyProperty[] inheritableProperties = TextSchema.GetInheritableProperties(typeof(Span)); for (int i = 0; i < inheritableProperties.Length; i++) { DependencyProperty property = inheritableProperties[i]; object inlineValue = inline.GetValue(property); object parentSpanValue = parentSpan.GetValue(property); if (!TextSchema.ValuesAreEqual(inlineValue, parentSpanValue)) { // Inner inline sets its own value for this property. We don't need to transfer it. continue; } object outerValue = parentSpan.Parent.GetValue(property); if (!TextSchema.ValuesAreEqual(inlineValue, outerValue)) { inline.SetValue(property, parentSpanValue); } } // Transfer non-inheritable properties // It only aims for the specific set of non-inheritable properties defined in TextSchema. // These properties are safe to be transferred from outer scope to inner scope. DependencyProperty[] nonInheritableProperties = TextSchema.GetNoninheritableProperties(typeof(Span)); for (int i = 0; i < nonInheritableProperties.Length; i++) { DependencyProperty property = nonInheritableProperties[i]; bool hasModifiers; // Check if the property value is default and not animated/coerced/data-bound. bool isParentValueDefault = ( parentSpan.GetValueSource(property, null, out hasModifiers) == BaseValueSourceInternal.Default && !hasModifiers ); bool isInlineValueDefault = ( inline.GetValueSource(property, null, out hasModifiers) == BaseValueSourceInternal.Default && !hasModifiers ); if (isInlineValueDefault && !isParentValueDefault) { inline.SetValue(property, parentSpan.GetValue(property)); } } // We can now remove the wrapping element parentSpan.Reposition(null, null); } else { // Parent of this inline cannot be deleted. Let's see what we can do with its parent inline = inline.Parent as Inline; } } } // Removes inline properties that affect formatting from the given range internal static void CharacterResetFormatting(TextPointer start, TextPointer end) { if (start.CompareTo(end) < 0) { // Split formatting elements at range boundaries start = SplitFormattingElements(start, /*keepEmptyFormatting:*/false, /*preserveStructuralFormatting*/true, /*limitingAncestor*/null); end = SplitFormattingElements(end, /*keepEmptyFormatting:*/false, /*preserveStructuralFormatting*/true, /*limitingAncestor*/null); while (start.CompareTo(end) < 0) { if (start.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart) { // When entering a next element check whether we should clear its inline properties. TextElement parent = (TextElement)start.Parent; // Note we do cleaning for Inline elements only - so properties set on Paragraphs // and other blocks will stay unchanged even if they set as local value. if (parent is Span && parent.ContentEnd.CompareTo(end) > 0) { // Preserve Hyperlink/Span properties when it is partially selected } // We can't assume that custom types derived from Span, once their formatting // properties are removed, can be transformed into a Span. So treat custom // types as inlines, even if they're derived from Span. else if (parent is Span && TextSchema.IsKnownType(parent.GetType())) { // Remember a position to merge inlines TextPointer mergePosition = parent.ElementStart; // Preserve only non-formatting properties of original span element. Span newSpan = TransferNonFormattingInlineProperties((Span)parent); if (newSpan != null) { newSpan.Reposition(parent.ElementStart, parent.ElementEnd); mergePosition = newSpan.ElementStart; } // Throw away original span parent.Reposition(null, null); // Now that content has changed, we must try to merge inlines at this position MergeFormattingInlines(mergePosition); } else if (parent is Inline) { ClearFormattingInlineProperties((Inline)parent); // Now that properties may be removed we must try to merge this element with a preceding one MergeFormattingInlines(parent.ElementStart); } } start = start.GetNextContextPosition(LogicalDirection.Forward); } // At the end try ro merge elements at end position MergeFormattingInlines(end); } } // Helper to clear formatting properties from passed inline element, preserving only non-formatting ones private static void ClearFormattingInlineProperties(Inline inline) { // Clear all properties from this inline element LocalValueEnumerator properties = inline.GetLocalValueEnumerator(); while (properties.MoveNext()) { DependencyProperty property = properties.Current.Property; // Skip readonly and non-formatting properties if (property.ReadOnly || TextSchema.IsNonFormattingCharacterProperty(property)) { continue; } inline.ClearValue(properties.Current.Property); } } // When source span has only character formatting properties, returns null. // Otherwise, when source span has at least one non-formatting character property (such as FlowDirection), // this helper returns a Span element preserving only such properties from source span. private static Span TransferNonFormattingInlineProperties(Span source) { Span span = null; DependencyProperty[] nonFormattingCharacterProperties = TextSchema.GetNonFormattingCharacterProperties(); for (int i = 0; i < nonFormattingCharacterProperties.Length; i++) { object value = source.GetValue(nonFormattingCharacterProperties[i]); object outerContextValue = ((ITextPointer)source.ElementStart).GetValue(nonFormattingCharacterProperties[i]); if (!TextSchema.ValuesAreEqual(value, outerContextValue)) { if (span == null) { span = new Span(); } span.SetValue(nonFormattingCharacterProperties[i], value); } } return span; } #endregion Character Formatting #region Paragraph Editing // .................................................................... // // Paragraph Editing // // .................................................................... // Splits the parent of the given breakPosition into two // elements with equivalent set of properties. internal static TextPointer SplitElement(TextPointer position) { TextElement element = (TextElement)position.Parent; if (position.IsFrozen) { position = new TextPointer(position); } TextElement newElement; if (position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd) { // A simple case when the new element can be added after the old one newElement = InsertElementClone(element.ElementEnd, element.ElementEnd, element); position.MoveToPosition(element.ElementEnd); } else if (position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart) { newElement = InsertElementClone(element.ElementStart, element.ElementStart, element); position.MoveToPosition(element.ElementStart); } else { newElement = InsertElementClone(position, element.ContentEnd, element); // Reposition the old element to the first half of content element.Reposition(element.ContentStart, newElement.ElementStart); position.MoveToPosition(element.ElementEnd); } Invariant.Assert(position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementEnd, "position must be after ElementEnd"); Invariant.Assert(position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementStart, "position must be before ElementStart"); return position; } ////// Insert paragraph break at the End position of a range. /// It only affects specified position - not a whole range. /// So it is essentially TextContainer-level (low-level) operation. /// /// /// Position at which the content should be split into two paragraphs. /// After the operation breakPosition moved into a beginning of the /// second paragraph after all opening tags created by splitting /// (this position may be not-normalized though if there are some /// other opening formatting tags following the position - this may /// be important for reading from xml when pasting point was before /// some opening formatting tags but after non-whitespace characters). /// /// /// True means that resulting TextPointer must be moved into the second paragraph. /// False means that resulting pointer remains in a non-normalized position /// between two paragraphs (or list items). /// ////// This function could be implemented from TextContainer class. /// ////// If position passed was in paragraph content, returns a TextPointer /// at an ContentStart of the second paragraph. /// If position passed was at a structural boundary (specifically table row end, /// block ui container start/end or before first table in a collection of blocks), /// then an implicit paragraph is inserted at the boundary and a position at its /// ContentStart is returned. /// internal static TextPointer InsertParagraphBreak(TextPointer position, bool moveIntoSecondParagraph) { Invariant.Assert(TextSchema.IsValidChildOfContainer(position.TextContainer.Parent.GetType(), typeof(Paragraph))); bool structuralBoundaryCrossed = TextPointerBase.IsAtRowEnd(position) || TextPointerBase.IsBeforeFirstTable(position) || TextPointerBase.IsInBlockUIContainer(position); if (position.Paragraph == null) { // Ensure insertion position, in case original position is not in text content. position = TextRangeEditTables.EnsureInsertionPosition(position); } Inline ancestor = position.GetNonMergeableInlineAncestor(); if (ancestor != null) { Invariant.Assert(TextPointerBase.IsPositionAtNonMergeableInlineBoundary(position), "Position must be at hyperlink boundary!"); // If position is at a hyperlink boundary, move outside hyperlink element scope // so that we can successfuly split formatting elements upto paragraph ancestor. position = position.IsAtNonMergeableInlineStart ? ancestor.ElementStart : ancestor.ElementEnd; } Paragraph paragraph = position.Paragraph; Invariant.Assert(paragraph != null, "Position must be in paragraph scope"); if (structuralBoundaryCrossed) { // In case structural boundary was crossed, an implicit paragraph was inserted in EnsureInsertionPosition. // No need to insert another paragraph break. return position; } TextPointer breakPosition = position; // Split all inline elements up to this paragraph breakPosition = SplitFormattingElements(breakPosition, /*keepEmptyFormatting:*/true); Invariant.Assert(breakPosition.Parent == paragraph, "breakPosition must be in paragraph scope after splitting formatting elements"); // Decide whether we need to split ListItem around this paragraph (if any). // We are splitting a list item if this paragraph is the only paragraph in a list item. // Otherwise we simply produce new paragraphs within the same list item. bool needToSplitListItem = TextPointerBase.GetImmediateListItem(paragraph.ContentStart) != null; breakPosition = SplitElement(breakPosition); // Also split ListItem (if any) if (needToSplitListItem) { Invariant.Assert(breakPosition.Parent is ListItem, "breakPosition must be in ListItem scope"); breakPosition = SplitElement(breakPosition); } if (moveIntoSecondParagraph) { // Move breakPosition inside of the second paragraph while (!(breakPosition.Parent is Paragraph) && breakPosition.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementStart) { breakPosition = breakPosition.GetNextContextPosition(LogicalDirection.Forward); } // Normalize with forward gravity breakPosition = breakPosition.GetInsertionPosition(LogicalDirection.Forward); } return breakPosition; } ////// Insert a LineBreak element at the given position. /// If position's parent is a Paragraph or Span, simply insert a LineBreak element at this position. /// Otherwise, ensure insertion position and insert a LineBreak element at insertion position in text content. /// /// /// ////// TextPointer positioned in the beginning of a Run immediately following a LineBreak inserted. /// internal static TextPointer InsertLineBreak(TextPointer position) { if (!TextSchema.IsValidChild(/*position*/position, /*childType*/typeof(LineBreak))) { // Ensure insertion position, in case position's parent is not a paragraph/span element. position = TextRangeEditTables.EnsureInsertionPosition(position); } if (TextSchema.IsInTextContent(position)) { // Split parent Run element, if position is inside of Run scope. position = SplitElement(position); } Invariant.Assert(TextSchema.IsValidChild(/*position*/position, /*childType*/typeof(LineBreak)), "position must be in valid scope now (span/paragraph) to insert a LineBreak element"); LineBreak lineBreak = new LineBreak(); position.InsertTextElement(lineBreak); return lineBreak.ElementEnd.GetInsertionPosition(LogicalDirection.Forward); } ////// Applies formatting properties for whole block elements. /// /// /// a position within first block in sequence /// /// /// a positionn within last block in sequence /// /// /// property changed on blocks /// /// /// value for the property /// internal static void SetParagraphProperty(TextPointer start, TextPointer end, DependencyProperty property, object value) { SetParagraphProperty(start, end, property, value, PropertyValueAction.SetValue); } ////// Applies formatting properties for whole block elements. /// /// /// a position within first block in sequence /// /// /// a positionn within last block in sequence /// /// /// property changed on blocks /// /// /// value for the property /// /// /// Specifies how to use the value - as absolute, as increment or a decrement. /// internal static void SetParagraphProperty(TextPointer start, TextPointer end, DependencyProperty property, object value, PropertyValueAction propertyValueAction) { Invariant.Assert(start != null, "null check: start"); Invariant.Assert(end != null, "null check: end"); Invariant.Assert(start.CompareTo(end) <= 0, "expecting: start <= end"); Invariant.Assert(property != null, "null check: property"); // Exclude last opening tag to avoid affecting a paragraph following the selection end = (TextPointer)TextRangeEdit.GetAdjustedRangeEnd(start, end); // Expand start pointer to the beginning of the first paragraph/blockuicontainer Block startParagraphOrBlockUIContainer = start.ParagraphOrBlockUIContainer; if (startParagraphOrBlockUIContainer != null) { start = startParagraphOrBlockUIContainer.ContentStart; } // Applying FlowDirection requires splitting all containing lists on the range boundaries // because the property is applied to whole List element (to affect bullet appearence) if (property == Block.FlowDirectionProperty) { // Split any boundary lists if needed. // We want to maintain the invariant that all lists and paragraphs within a list, have the same FlowDirection value. // If paragraph FlowDirection command requests a different value of FlowDirection on parts of a list, // we split the list to maintain this invariant. if (!TextRangeEditLists.SplitListsForFlowDirectionChange(start, end, value)) { // If lists at start and end cannot be split successfully, we cannot apply FlowDirection property to the paragraph content. return; } // And expand range start to the beginning of the containing list ListItem listItem = start.GetListAncestor(); if (listItem != null && listItem.List != null) { start = listItem.List.ElementStart; } } // Walk all paragraphs in the affected segment. For FlowDirection property, also walk lists. SetParagraphPropertyWorker(start, end, property, value, propertyValueAction); } // Worker for SetParagraphProperty, iterates over Blocks recursively. private static void SetParagraphPropertyWorker(TextPointer start, TextPointer end, DependencyProperty property, object value, PropertyValueAction propertyValueAction) { Block block = GetNextBlock(start, end); while (block != null) { if (TextSchema.IsParagraphOrBlockUIContainer(block.GetType())) { // Get the parent to check the parent FlowDirection with current DependencyObject parent = start.TextContainer.Parent; SetPropertyOnParagraphOrBlockUIContainer(parent, block, property, value, propertyValueAction); // Go to paragraph/BUIC end position, normalize forward start = block.ElementEnd.GetPositionAtOffset(0, LogicalDirection.Forward); } else if (block is List) { // Apply property value to content first, recursively, since // (potentially) setting FlowDirection on the parent List will // affect child elements. TextPointer contentStart = block.ContentStart.GetPositionAtOffset(0, LogicalDirection.Forward); // Normalize forward; contentStart = contentStart.GetNextContextPosition(LogicalDirection.Forward); // Leave scope of initial List. TextPointer contentEnd = block.ContentEnd; SetParagraphPropertyWorker(contentStart, contentEnd, property, value, propertyValueAction); // Special cases for applying paragraph properties to Lists if (property == Block.FlowDirectionProperty) { // Set FlowDirection property on List SetPropertyValue(block, property, /*currentValue:*/block.GetValue(property), /*newValue:*/value); // For flow direction command, we also swap Left and Right margins of the list. // This ensures indentation is mirrored correctly. SwapBlockLeftAndRightMargins(block); } // Go to end position, normalize forward. start = block.ElementEnd.GetPositionAtOffset(0, LogicalDirection.Forward); } block = GetNextBlock(start, end); } } // Helper for SetParagraphProperty -- applies given property value to passed block element. private static void SetPropertyOnParagraphOrBlockUIContainer(DependencyObject parent, Block block, DependencyProperty property, object value, PropertyValueAction propertyValueAction) { // Get the parent flow direction FlowDirection parentFlowDirection; if (parent != null) { parentFlowDirection = (FlowDirection)parent.GetValue(FrameworkElement.FlowDirectionProperty); } else { parentFlowDirection = (FlowDirection)FrameworkElement.FlowDirectionProperty.GetDefaultValue(typeof(FrameworkElement)); } // Some of paragraph operations depend on its flow direction, so get it first. FlowDirection flowDirection = (FlowDirection)block.GetValue(Block.FlowDirectionProperty); // Inspect a property value for this paragraph object currentValue = block.GetValue(property); object newValue = value; // If we're setting a structural property on a Paragraph, we need to preserve // the current value on its children. PreserveBlockContentStructuralProperty(block, property, currentValue, value); if (property.PropertyType == typeof(Thickness)) { // For Margin, Padding, Border - apply the following logic: Invariant.Assert(currentValue is Thickness, "Expecting the currentValue to be of Thinkness type"); Invariant.Assert(newValue is Thickness, "Expecting the newValue to be of Thinkness type"); newValue = ComputeNewThicknessValue((Thickness)currentValue, (Thickness)newValue, parentFlowDirection, flowDirection, propertyValueAction); } else if (property == Paragraph.TextAlignmentProperty) { Invariant.Assert(value is TextAlignment, "Expecting TextAlignment as a value of a Paragraph.TextAlignmentProperty"); // TextAlignment must be reverted for RightToLeft flow direction newValue = ComputeNewTextAlignmentValue((TextAlignment)value, flowDirection); // For BlockUIContainer text alignment must be translated into // HorizontalAlignment of the child embedded object. if (block is BlockUIContainer) { UIElement embeddedElement = ((BlockUIContainer)block).Child; if (embeddedElement != null) { HorizontalAlignment horizontalAlignment = GetHorizontalAlignmentFromTextAlignment((TextAlignment)newValue); // Create an undo unit for property change on embedded framework element. UIElementPropertyUndoUnit.Add(block.TextContainer, embeddedElement, FrameworkElement.HorizontalAlignmentProperty, horizontalAlignment); embeddedElement.SetValue(FrameworkElement.HorizontalAlignmentProperty, horizontalAlignment); } } } else if (currentValue is double) { newValue = NewValue((double)currentValue, (double)newValue, propertyValueAction); } SetPropertyValue(block, property, currentValue, newValue); if (property == Block.FlowDirectionProperty) { // For flow direction command, we also swap Left and Right margins of the paragraph. // This ensures indentation is mirrored correctly. SwapBlockLeftAndRightMargins(block); } } // Helper for SetPropertyOnParagraphOrBlockUIContainer. // // When setting a structural property on a Block, we must be careful to preserve // the current value on its children. // // A structural character property is more strict for its scope than other (non-structural) inline properties (such as fontweight). // While the associativity rule holds true for non-structural properties when there values are equal, // (FontWeight)A (FontWeight)B == (FontWeight) AB // this does not hold true for structual properties even when there values may be equal, // (FlowDirection)A (FlowDirection)B != (FlowDirection)A B private static void PreserveBlockContentStructuralProperty(Block block, DependencyProperty property, object currentValue, object newValue) { Paragraph paragraph = block as Paragraph; if (paragraph != null && TextSchema.IsStructuralCharacterProperty(property) && !TextSchema.ValuesAreEqual(currentValue, newValue)) { // First drill down to the first run of multiple children, or the first // single child with a local value. Inline firstChild = paragraph.Inlines.FirstInline; Inline lastChild = paragraph.Inlines.LastInline; while (firstChild != null && firstChild == lastChild && firstChild is Span && !HasLocalPropertyValue(firstChild, property)) { firstChild = ((Span)firstChild).Inlines.FirstInline; lastChild = ((Span)lastChild).Inlines.LastInline; } // Set the old value on the existing content. if (firstChild != lastChild) { Inline nextChild; do { // Find a run of children with the same property value. object firstChildValue = firstChild.GetValue(property); lastChild = firstChild; while (true) { nextChild = (Inline)lastChild.NextElement; if (nextChild == null) break; if (!TextSchema.ValuesAreEqual(nextChild.GetValue(property), firstChildValue)) break; lastChild = nextChild; } if (TextSchema.ValuesAreEqual(firstChildValue, currentValue)) { if (firstChild != lastChild) { // Wrap multiple children in a new Span with the old value. TextPointer start = firstChild.ElementStart.GetFrozenPointer(LogicalDirection.Backward); TextPointer end = lastChild.ElementEnd.GetFrozenPointer(LogicalDirection.Forward); // Because SetStructuralInlineProperty doesn't know that we're about to change the Paragraph's // property value, it will optimize away Spans. We still want to use it though, to canonicalize // the content. SetStructuralInlineProperty(start, end, property, currentValue); firstChild = (Inline)start.GetAdjacentElement(LogicalDirection.Forward); lastChild = (Inline)end.GetAdjacentElement(LogicalDirection.Backward); if (firstChild != lastChild) { Span span = firstChild.Parent as Span; if (span == null || span.Inlines.FirstInline != firstChild || span.Inlines.LastInline != lastChild) { span = new Span(firstChild.ElementStart, lastChild.ElementEnd); } span.SetValue(property, currentValue); } } if (firstChild == lastChild) { SetStructuralPropertyOnInline(firstChild, property, currentValue); } } firstChild = nextChild; } while (firstChild != null); } else { // If the only child is a Run, set the value directly. // Otherwise there's no need to set the value. SetStructuralPropertyOnInline(firstChild, property, currentValue); } } } // Helper for PreserveBlockContentStructuralProperty. private static void SetStructuralPropertyOnInline(Inline inline, DependencyProperty property, object value) { if (inline is Run && !inline.IsEmpty && !HasLocalPropertyValue(inline, property)) { // If the only child is a Run, set the value directly. // Otherwise there's no need to set the value. inline.SetValue(property, value); } } // Finds a Paragraph/BlockUIContainer/List element with ElementStart before or at the given pointer // Creates implicit paragraphs at potential paragraph positions if needed private static Block GetNextBlock(TextPointer pointer, TextPointer limit) { Block block = null; while (pointer != null && pointer.CompareTo(limit) <= 0) { if (pointer.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart) { block = pointer.Parent as Block; if (block is Paragraph || block is BlockUIContainer || block is List) { break; } } if (TextPointerBase.IsAtPotentialParagraphPosition(pointer)) { pointer = TextRangeEditTables.EnsureInsertionPosition(pointer); block = pointer.Paragraph; Invariant.Assert(block != null); break; } // Advance the scanning pointer pointer = pointer.GetNextContextPosition(LogicalDirection.Forward); } return block; } // Helper for SetParagraphProperty private static Thickness ComputeNewThicknessValue(Thickness currentThickness, Thickness newThickness, FlowDirection parentFlowDirection, FlowDirection flowDirection, PropertyValueAction propertyValueAction) { // Negative value for particular axis means "leave it unchanged" double topMargin = newThickness.Top < 0 ? currentThickness.Top : NewValue(currentThickness.Top, newThickness.Top, propertyValueAction); double bottomMargin = newThickness.Bottom < 0 ? currentThickness.Bottom : NewValue(currentThickness.Bottom, newThickness.Bottom, propertyValueAction); double leftMargin; double rightMargin; if (parentFlowDirection != flowDirection) { // In case of mismatching FlowDirection between parent and current, // we apply value.Left to currentValue.Right and vice versa. // The caller of the method must account for that and use Left/Right margins appropriately. leftMargin = newThickness.Right < 0 ? currentThickness.Left : NewValue(currentThickness.Left, newThickness.Right, propertyValueAction); rightMargin = newThickness.Left < 0 ? currentThickness.Right : NewValue(currentThickness.Right, newThickness.Left, propertyValueAction); } else { leftMargin = newThickness.Left < 0 ? currentThickness.Left : NewValue(currentThickness.Left, newThickness.Left, propertyValueAction); rightMargin = newThickness.Right < 0 ? currentThickness.Right : NewValue(currentThickness.Right, newThickness.Right, propertyValueAction); } return new Thickness(leftMargin, topMargin, rightMargin, bottomMargin); } // Helper for SetParagraphProperty, flips TextAligment values when FlowDirection is RTL. private static TextAlignment ComputeNewTextAlignmentValue(TextAlignment textAlignment, FlowDirection flowDirection) { if (textAlignment == TextAlignment.Left) { textAlignment = (flowDirection == FlowDirection.LeftToRight) ? TextAlignment.Left : TextAlignment.Right; } else if (textAlignment == TextAlignment.Right) { textAlignment = (flowDirection == FlowDirection.LeftToRight) ? TextAlignment.Right : TextAlignment.Left; } return textAlignment; } // Applies newValue to the currentValue according to a propertyValueAction - // increments or just sets it. private static double NewValue(double currentValue, double newValue, PropertyValueAction propertyValueAction) { if (double.IsNaN(newValue)) { return newValue; } Invariant.Assert(newValue >= 0); if (double.IsNaN(currentValue)) { currentValue = 0.0; } newValue = propertyValueAction == PropertyValueAction.IncreaseByAbsoluteValue ? currentValue + newValue : propertyValueAction == PropertyValueAction.DecreaseByAbsoluteValue ? currentValue - newValue : propertyValueAction == PropertyValueAction.IncreaseByPercentageValue ? currentValue * (1.0 + newValue / 100) : propertyValueAction == PropertyValueAction.DecreaseByPercentageValue ? currentValue * (1.0 - newValue / 100) : newValue; if (newValue < 0.0) { newValue = 0.0; } return newValue; } // Translates TextAlignment value into corresponding HorizontalAlignment value. // Used in applying Paragraph.TextAlignmentProperty to BlockUIContainer elements. internal static HorizontalAlignment GetHorizontalAlignmentFromTextAlignment(TextAlignment textAlignment) { HorizontalAlignment horizontalAlignment; switch (textAlignment) { default: case TextAlignment.Left: horizontalAlignment = HorizontalAlignment.Left; break; case TextAlignment.Center: horizontalAlignment = HorizontalAlignment.Center; break; case TextAlignment.Right: horizontalAlignment = HorizontalAlignment.Right; break; case TextAlignment.Justify: horizontalAlignment = HorizontalAlignment.Stretch; break; } return horizontalAlignment; } // Translates HorizontalAlignment value into corresponding TextAlignment value. internal static TextAlignment GetTextAlignmentFromHorizontalAlignment(HorizontalAlignment horizontalAlignment) { TextAlignment textAlignment; switch (horizontalAlignment) { case HorizontalAlignment.Left: textAlignment = TextAlignment.Left; break; case HorizontalAlignment.Center: textAlignment = TextAlignment.Center; break; case HorizontalAlignment.Right: textAlignment = TextAlignment.Right; break; default: case HorizontalAlignment.Stretch: textAlignment = TextAlignment.Justify; break; } return textAlignment; } // Helper to set property value on element. private static void SetPropertyValue(TextElement element, DependencyProperty property, object currentValue, object newValue) { if (!TextSchema.ValuesAreEqual(newValue, currentValue)) { // first clear and see if it will do element.ClearValue(property); // if still need it, set it if (!TextSchema.ValuesAreEqual(newValue, element.GetValue(property))) { element.SetValue(property, newValue); } } } // Helper that swaps the left and right margins of a block element. private static void SwapBlockLeftAndRightMargins(Block block) { object value = block.GetValue(Block.MarginProperty); if (value is Thickness) { if (Paragraph.IsMarginAuto((Thickness)value)) { // Nothing to do for auto thickess } else { // Swap left and right values object newValue = new Thickness( /*left*/((Thickness)value).Right, /*top:*/((Thickness)value).Top, /*right:*/((Thickness)value).Left, /*bottom:*/((Thickness)value).Bottom); SetPropertyValue(block, Block.MarginProperty, value, newValue); } } } // Returns a pointer of a text range adjusted so it does not affect // the paragraph following the selection. internal static ITextPointer GetAdjustedRangeEnd(ITextPointer rangeStart, ITextPointer rangeEnd) { if (rangeStart.CompareTo(rangeEnd) < 0 && rangeEnd.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart) { rangeEnd = rangeEnd.GetNextInsertionPosition(LogicalDirection.Backward); if (rangeEnd == null) { rangeEnd = rangeStart; // Recover position for container start case - we never return null from this method. } } else if (TextPointerBase.IsAfterLastParagraph(rangeEnd)) { rangeEnd = rangeEnd.GetInsertionPosition(LogicalDirection.Backward); } return rangeEnd; } // Merges Spans or Runs with equal FlowDirection that border at a given position. internal static void MergeFlowDirection(TextPointer position) { TextPointerContext backwardContext = position.GetPointerContext(LogicalDirection.Backward); TextPointerContext forwardContext = position.GetPointerContext(LogicalDirection.Forward); if (!(backwardContext == TextPointerContext.ElementStart || backwardContext == TextPointerContext.ElementEnd) && !(forwardContext == TextPointerContext.ElementStart || forwardContext == TextPointerContext.ElementEnd)) { // Early out if position is not at an Inline border. return; } // Find the common ancestor of the two adjacent content runs. while (position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && TextSchema.IsMergeableInline(position.Parent.GetType())) { position = ((Inline)position.Parent).ElementStart; } while (position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd && TextSchema.IsMergeableInline(position.Parent.GetType())) { position = ((Inline)position.Parent).ElementEnd; } TextElement commonAncestor = position.Parent as TextElement; if (!(commonAncestor is Span || commonAncestor is Paragraph)) { // Don't try to merge across Block boundaries. return; } // Find the previous content. TextPointer previousPosition = position.CreatePointer(); while (previousPosition.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementEnd && TextSchema.IsMergeableInline(previousPosition.GetAdjacentElement(LogicalDirection.Backward).GetType())) { previousPosition = ((Inline)previousPosition.GetAdjacentElement(LogicalDirection.Backward)).ContentEnd; } Run previousRun = previousPosition.Parent as Run; // Find the next content. TextPointer nextPosition = position.CreatePointer(); while (nextPosition.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementStart && TextSchema.IsMergeableInline(nextPosition.GetAdjacentElement(LogicalDirection.Forward).GetType())) { nextPosition = ((Inline)nextPosition.GetAdjacentElement(LogicalDirection.Forward)).ContentStart; } Run nextRun = nextPosition.Parent as Run; if (previousRun == null || previousRun.IsEmpty || nextRun == null || nextRun.IsEmpty) { // No text to make the merge meaningful. return; } FlowDirection midpointFlowDirection = (FlowDirection)commonAncestor.GetValue(FrameworkElement.FlowDirectionProperty); FlowDirection previousFlowDirection = (FlowDirection)previousRun.GetValue(FrameworkElement.FlowDirectionProperty); FlowDirection nextFlowDirection = (FlowDirection)nextRun.GetValue(FrameworkElement.FlowDirectionProperty); // If the previous and next content have the same FlowDirection, but their // common ancestor differs, we want to merge them. if (previousFlowDirection == nextFlowDirection && previousFlowDirection != midpointFlowDirection) { // Expand the context out to include any scoping Spans with local FlowDirection. Inline scopingPreviousInline = GetScopingFlowDirectionInline(previousRun); Inline scopingNextInline = GetScopingFlowDirectionInline(nextRun); // Set a single FlowDirection Span over the whole lot of it. SetStructuralInlineProperty(scopingPreviousInline.ElementStart, scopingNextInline.ElementEnd, FrameworkElement.FlowDirectionProperty, previousFlowDirection); } } // Returns false if calling ApplyStructuralInlineProperty will throw an InvalidOperationException with the // same input parameters. // // In practice, this method returns false when the property apply would require that we split a // non-mergeable Inline such as Hyperlink. internal static bool CanApplyStructuralInlineProperty(TextPointer start, TextPointer end) { return ValidateApplyStructuralInlineProperty(start, end, TextPointer.GetCommonAncestor(start, end), null); } // ..................................................................... // // Paragraph Editing Commands // // ..................................................................... ////// Increments/decrements paragraph leading maring property. /// For LeftToRight paragraphs a leading maring is the left marinng, /// for RightToLeft paragraphs it is the right maring. /// /// /// /// /// Must be one of IncreaseValue or DecreaseValue. /// internal static void IncrementParagraphLeadingMargin(TextRange range, double increment, PropertyValueAction propertyValueAction) { Invariant.Assert(increment >= 0); Invariant.Assert(propertyValueAction != PropertyValueAction.SetValue); if (increment == 0) { // Nothing to do. Just return. return; } // Note that SetParagraphProperty method will swap Left and Right margins for RightToLeft paragraphs. // Note that -1 values for Thickness axis means leaving its value as is. Thickness thickness = new Thickness(increment, -1, -1, -1); // Apply paragraph margin property TextRangeEdit.SetParagraphProperty(range.Start, range.End, Block.MarginProperty, thickness, propertyValueAction); } ////// Deletes a content covered by two positions assuming that /// the content crosses only inline boundaries (if at all) - /// no Paragraph or any other Block or structural elements are /// supposed to be crossed (including Floaters and Figures). /// /// /// internal static void DeleteInlineContent(ITextPointer start, ITextPointer end) { DeleteParagraphContent(start, end); } ////// Deletes a content covered by two positions assuming that /// the content crosses only paragraph-mergeable boundaries (if at all) - /// Paragraphs, Sections, Lists, ListItems, but not harder structural /// elements like Tables, TableCells, TableRows, Floaters, Figures. /// /// /// Position indicating a beginning of deleted content. /// /// /// Position indicating an end of deleted content. /// internal static void DeleteParagraphContent(ITextPointer start, ITextPointer end) { // Parameters validation Invariant.Assert(start != null, "null check: start"); Invariant.Assert(end != null, "null check: end"); Invariant.Assert(start.CompareTo(end) <= 0, "expecting: start <= end"); if (!(start is TextPointer)) { // Abstract text container. We can only use basic abstract functionality here: start.DeleteContentToPosition(end); return; } TextPointer startPosition = (TextPointer)start; TextPointer endPosition = (TextPointer)end; // Delete all equi-scoped content in the given range DeleteEquiScopedContent(startPosition, endPosition); // delete content runs from start to root DeleteEquiScopedContent(endPosition, startPosition); // delete contentruns from end to root // Merge crossed elements if (startPosition.CompareTo(endPosition) < 0) { if (TextPointerBase.IsAfterLastParagraph(endPosition)) { // This means that end position is after the last paragraph of a text container. // When the last paragraph is empty (and selection crosses its end boundary) // we need to delete it. // When last paragraph is not empty, we have to leave it as is. while (startPosition.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && startPosition.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd) { // TextElement parent = (TextElement)startPosition.Parent; if (parent is Inline || TextSchema.AllowsParagraphMerging(parent.GetType())) { parent.RepositionWithContent(null); } else { break; } } } else { Block firstParagraphOrBlockUIContainer = startPosition.ParagraphOrBlockUIContainer; Block secondParagraphOrBlockUIContainer = endPosition.ParagraphOrBlockUIContainer; // If startPosition and/or endPosition is parented by an empty ListItem, create an implicit paragraph in it. // This will enable the following code to merge paragraphs in list items. if (firstParagraphOrBlockUIContainer == null && TextPointerBase.IsInEmptyListItem(startPosition)) { startPosition = TextRangeEditTables.EnsureInsertionPosition(startPosition); firstParagraphOrBlockUIContainer = startPosition.Paragraph; Invariant.Assert(firstParagraphOrBlockUIContainer != null, "EnsureInsertionPosition must create a paragraph inside list item - 1"); } if (secondParagraphOrBlockUIContainer == null && TextPointerBase.IsInEmptyListItem(endPosition)) { endPosition = TextRangeEditTables.EnsureInsertionPosition(endPosition); secondParagraphOrBlockUIContainer = endPosition.Paragraph; Invariant.Assert(secondParagraphOrBlockUIContainer != null, "EnsureInsertionPosition must create a paragraph inside list item - 2"); } if (firstParagraphOrBlockUIContainer != null && secondParagraphOrBlockUIContainer != null) { TextRangeEditLists.MergeParagraphs(firstParagraphOrBlockUIContainer, secondParagraphOrBlockUIContainer); } else { // When crossing BlockUIContainer boundaries we need to clear // any empty BlockUIContainers and empty adjacent paragraphs MergeEmptyParagraphsAndBlockUIContainers(startPosition, endPosition); } } } // Remove empty formatting elements MergeFormattingInlines(startPosition); MergeFormattingInlines(endPosition); // Check for remaining empty BlockUICOntainer or empty Hyperlink elements if (startPosition.Parent is BlockUIContainer && ((BlockUIContainer)startPosition.Parent).IsEmpty) { ((BlockUIContainer)startPosition.Parent).Reposition(null, null); } else if (startPosition.Parent is Hyperlink && ((Hyperlink)startPosition.Parent).IsEmpty) { ((Hyperlink)startPosition.Parent).Reposition(null, null); // After deleting an empty hyperlink, we might have inlines to merge. MergeFormattingInlines(startPosition); } // } // Helper for DeleteParagraphContent // Takes 2 positions possibly parented by paragraph or BlockUIContainer // and deletes them if they are empty . private static void MergeEmptyParagraphsAndBlockUIContainers(TextPointer startPosition, TextPointer endPosition) { Block first = startPosition.ParagraphOrBlockUIContainer; Block second = endPosition.ParagraphOrBlockUIContainer; if (first is BlockUIContainer) { if (first.IsEmpty) { first.Reposition(null, null); return; } else if (second is Paragraph && Paragraph.HasNoTextContent((Paragraph) second)) { second.RepositionWithContent(null); return; } } if (second is BlockUIContainer) { if (second.IsEmpty) { second.Reposition(null, null); return; } else if (second is Paragraph && Paragraph.HasNoTextContent((Paragraph) first)) { first.RepositionWithContent(null); return; } } } ////// Deletes all equi-scoped segments of content from start TextPointer /// up to fragment root. Thus clears one half of a fragment. /// The other half remains untouched. /// All elements whose boundaries are crossed by this range /// remain in the tree (except for emptied formatting elements). /// /// /// A position from which content clearinng starts. /// All content segments between this position and a fragment /// root will be deleted. /// /// /// A position indicating the other boundary of a fragment. /// This position is used for fragment root identification. /// private static void DeleteEquiScopedContent(TextPointer start, TextPointer end) { // Validate parameters Invariant.Assert(start != null, "null check: start"); Invariant.Assert(end != null, "null check: end"); if (start.CompareTo(end) == 0) { return; } if (start.Parent == end.Parent) { DeleteContentBetweenPositions(start, end); return; } // Identify directional parameters LogicalDirection direction; LogicalDirection oppositeDirection; TextPointerContext enterScopeSymbol; TextPointerContext leaveScopeSymbol; ElementEdge edgeBeforeElement; ElementEdge edgeAfterElement; if (start.CompareTo(end) < 0) { direction = LogicalDirection.Forward; oppositeDirection = LogicalDirection.Backward; enterScopeSymbol = TextPointerContext.ElementStart; leaveScopeSymbol = TextPointerContext.ElementEnd; edgeBeforeElement = ElementEdge.BeforeStart; edgeAfterElement = ElementEdge.AfterEnd; } else { direction = LogicalDirection.Backward; oppositeDirection = LogicalDirection.Forward; enterScopeSymbol = TextPointerContext.ElementEnd; leaveScopeSymbol = TextPointerContext.ElementStart; edgeBeforeElement = ElementEdge.AfterEnd; edgeAfterElement = ElementEdge.BeforeStart; } // previousPosition will store a location where nondeleted content starts TextPointer previousPosition = new TextPointer(start); // nextPosition runs toward other end until level change - // so that we could delete all content from previousPosition // to nextPosition at once. TextPointer nextPosition = new TextPointer(start); // Run nextPosition forward until the very end of affected range while (nextPosition.CompareTo(end) != 0) { Invariant.Assert(direction == LogicalDirection.Forward && nextPosition.CompareTo(end) < 0 || direction == LogicalDirection.Backward && nextPosition.CompareTo(end) > 0, "Inappropriate position ordering"); Invariant.Assert(previousPosition.Parent == nextPosition.Parent, "inconsistent position Parents: previous and next"); TextPointerContext pointerContext = nextPosition.GetPointerContext(direction); if (pointerContext == TextPointerContext.Text || pointerContext == TextPointerContext.EmbeddedElement) { // Add this run to a collection of equi-scoped content nextPosition.MoveToNextContextPosition(direction); // Check if we went too far and return a little to end if necessary if (direction == LogicalDirection.Forward && nextPosition.CompareTo(end) > 0 || direction == LogicalDirection.Backward && nextPosition.CompareTo(end) < 0) { Invariant.Assert(nextPosition.Parent == end.Parent, "inconsistent poaition Parents: next and end"); nextPosition.MoveToPosition(end); break; } } else if (pointerContext == enterScopeSymbol) { // Jump over the element and continue collecting equi-scoped content nextPosition.MoveToNextContextPosition(direction); ((ITextPointer)nextPosition).MoveToElementEdge(edgeAfterElement); // If our range crosses the element then we stop before its opening tag if (direction == LogicalDirection.Forward && nextPosition.CompareTo(end) >= 0 || direction == LogicalDirection.Backward && nextPosition.CompareTo(end) <= 0) { nextPosition.MoveToNextContextPosition(oppositeDirection); ((ITextPointer)nextPosition).MoveToElementEdge(edgeBeforeElement); break; } } else if (pointerContext == leaveScopeSymbol) { // Delete preceding content and continue on outer level DeleteContentBetweenPositions(previousPosition, nextPosition); if (!ExtractEmptyFormattingElements(previousPosition)) { // Continue on outer level Invariant.Assert(nextPosition.GetPointerContext(direction) == leaveScopeSymbol, "Unexpected context of nextPosition"); nextPosition.MoveToNextContextPosition(direction); } previousPosition.MoveToPosition(nextPosition); } else { Invariant.Assert(false, "Not expecting None context here"); Invariant.Assert(pointerContext == TextPointerContext.None, "Unknown pointer context"); break; } } Invariant.Assert(previousPosition.Parent == nextPosition.Parent, "inconsistent Parents: previousPosition, nextPosition"); DeleteContentBetweenPositions(previousPosition, nextPosition); } ////// Helper for TextContainer.DeleteContent allowing arbitrary /// order of positions and doinng nothing in case of empty range. /// Removes remaining empty formatting elements - if they not inside empty blocks. /// /// /// One of content boundary positions. May precede or follow the TextPointer two. /// Must belong to the same scope as TextPointer two. /// /// /// Another content boundary position. May precede or follow the TextPointer one. /// Must belong to the same scope as TextPointer one. /// ////// true if surrounding formatting elements have beed deleted as a side effect. /// private static bool DeleteContentBetweenPositions(TextPointer one, TextPointer two) { Invariant.Assert(one.Parent == two.Parent, "inconsistent Parents: one and two"); if (one.CompareTo(two) < 0) { one.TextContainer.DeleteContentInternal(one, two); } else if (one.CompareTo(two) > 0) { two.TextContainer.DeleteContentInternal(two, one); } Invariant.Assert(one.CompareTo(two) == 0, "Positions one and two must be equal now"); return false; } #endregion Paragraph Editing #endregion Internal Methods // -------------------------------------------------------------------- // // Private Methods // // ------------------------------------------------------------------- #region Private Methods private static TextPointer SplitFormattingElements(TextPointer splitPosition, bool keepEmptyFormatting, TextElement limitingAncestor) { return SplitFormattingElements(splitPosition, keepEmptyFormatting, /*preserveStructuralFormatting*/false, limitingAncestor); } ////// Splits all inline element walking up to specified limitingAncestor. /// limitingAncestor remains unsplit. /// /// /// Position at which splitting happens. After the operation the position /// is between split elements - scoped by limitingElement (if it is not frozen). /// /// /// Flag to indicate whether split operation should create empty formatting tags. /// /// /// If true, ensures that structural properties are preserved on elements. Runs will be split /// after creating a wrapping Span preserving the original structural property value, otherwise /// splitting will halt when a non-Run element has a local structural property (as if a limiting /// ancestor or non-mergeable inline had been encountered). /// /// /// If null, this has no impact on split operation. /// Otherwise, this method ensures that this ancestor boundary is not crossed while splitting. /// ////// TextPointer positioned in between two elements. /// It may be the same instance as splitPosition parameter /// (in case if it was not frozen), or some new instance of TextPointer. /// private static TextPointer SplitFormattingElements(TextPointer splitPosition, bool keepEmptyFormatting, bool preserveStructuralFormatting, TextElement limitingAncestor) { if (preserveStructuralFormatting) { Run run = splitPosition.Parent as Run; if (run != null && run != limitingAncestor && HasLocalInheritableStructuralPropertyValue(run)) { // This Run has a structural property set on it (eg, FlowDirection) which cannot simply be split // (two adjacent Runs with the same FlowDirection will render differently than a single Run with // the same value, when the parent FlowDirection property differs). // So create a wrapping Span which will survive in the loop below. Span span = new Span(run.ElementStart, run.ElementEnd); TransferStructuralProperties(run, span); } } // Splitting loop: cutting a parent element until we reach the non-inline, // never crossing ancestor boundary. while (splitPosition.Parent != null && TextSchema.IsMergeableInline(splitPosition.Parent.GetType()) && splitPosition.Parent != limitingAncestor && (!preserveStructuralFormatting || !HasLocalInheritableStructuralPropertyValue((Inline)splitPosition.Parent))) { splitPosition = SplitFormattingElement(splitPosition, keepEmptyFormatting); } return splitPosition; } // Copies all structural properties from source (clearing the property) to destination. private static void TransferStructuralProperties(Inline source, Inline destination) { bool sourceIsChild = (source.Parent == destination); for (int i = 0; i < TextSchema.StructuralCharacterProperties.Length; i++) { DependencyProperty property = TextSchema.StructuralCharacterProperties[i]; if ((sourceIsChild && HasLocalInheritableStructuralPropertyValue(source)) || (!sourceIsChild && HasLocalStructuralPropertyValue(source))) { object value = source.GetValue(property); source.ClearValue(property); destination.SetValue(property, value); } } } // Returns true if an Inline has one or more non-readonly local property values. private static bool HasWriteableLocalPropertyValues(Inline inline) { LocalValueEnumerator enumerator = inline.GetLocalValueEnumerator(); bool hasLocalValues = false; while (!hasLocalValues && enumerator.MoveNext()) { hasLocalValues = !enumerator.Current.Property.ReadOnly; } return hasLocalValues; } // Returns true if an inline has one or more structural local property values. private static bool HasLocalInheritableStructuralPropertyValue(Inline inline) { int i; for (i = 0; i < TextSchema.StructuralCharacterProperties.Length; i++) { DependencyProperty inheritableProperty = TextSchema.StructuralCharacterProperties[i]; if (!TextSchema.ValuesAreEqual(inline.GetValue(inheritableProperty), inline.Parent.GetValue(inheritableProperty))) break; } return (i < TextSchema.StructuralCharacterProperties.Length); } // Returns true if an inline has one or more structural local property values. private static bool HasLocalStructuralPropertyValue(Inline inline) { int i; for (i = 0; i < TextSchema.StructuralCharacterProperties.Length; i++) { DependencyProperty inheritableProperty = TextSchema.StructuralCharacterProperties[i]; if (HasLocalPropertyValue(inline, inheritableProperty)) break; } return (i < TextSchema.StructuralCharacterProperties.Length); } // Returns true if an inline has a local property value with higher precedence than inheritance. private static bool HasLocalPropertyValue(Inline inline, DependencyProperty property) { bool hasModifiers; BaseValueSourceInternal source = inline.GetValueSource(property, null, out hasModifiers); return (source != BaseValueSourceInternal.Unknown && source != BaseValueSourceInternal.Default && source != BaseValueSourceInternal.Inherited); } // Helper for MergeFlowDirection. Returns a greatest scoping Inline of a Run // with matching FlowDirection. The caller guarantees that a scoping Span // has differing FlowDirection. private static Inline GetScopingFlowDirectionInline(Run run) { FlowDirection flowDirection = run.FlowDirection; Inline inline = run; while ((FlowDirection)inline.Parent.GetValue(FrameworkElement.FlowDirectionProperty) == flowDirection) { inline = (Span)inline.Parent; } return inline; } // Helper to set non-structural Inline property to a range between start and end positions. private static void SetNonStructuralInlineProperty(TextPointer start, TextPointer end, DependencyProperty formattingProperty, object value, PropertyValueAction propertyValueAction) { // Split formatting elements at range boundaries start = SplitFormattingElements(start, /*keepEmptyFormatting:*/false, /*preserveStructuralFormatting*/true, /*limitingAncestor*/null); end = SplitFormattingElements(end, /*keepEmptyFormatting:*/false, /*preserveStructuralFormatting*/true, /*limitingAncestor*/null); Run run = TextRangeEdit.GetNextRun(start, end); while (run != null) { object currentValue = run.GetValue(formattingProperty); object newValue = value; if (propertyValueAction != PropertyValueAction.SetValue) { Invariant.Assert(formattingProperty == TextElement.FontSizeProperty, "Only FontSize can be incremented/decremented among character properties"); newValue = GetNewFontSizeValue((double)currentValue, (double)value, propertyValueAction); } // Set new property value SetPropertyValue(run, formattingProperty, currentValue, newValue); // Remember a position after the current run for the following processing. // Normalize forward since Run.ElementEnd has backward gravity. TextPointer nextRunPosition = run.ElementEnd.GetPositionAtOffset(0, LogicalDirection.Forward); if (TextPointerBase.IsAtPotentialRunPosition(run)) { // If current run was an implicit run, we move to the next context position after its element end. // This is safe because by definition of IsAtPotentialRunPosition predicate, // our current run can never have an adjacent run element or // another adjacent potential run position. nextRunPosition = nextRunPosition.GetNextContextPosition(LogicalDirection.Forward); } // Merge this run with the previous one. // Note that this can affect text structure even after this run. MergeFormattingInlines(run.ContentStart); // Find the next Run to process run = TextRangeEdit.GetNextRun(nextRunPosition, end); } MergeFormattingInlines(end); } // Helper to calculate new value of Run.FontSize property when PropertyValueAction is increment/decrement. private static double GetNewFontSizeValue(double currentValue, double value, PropertyValueAction propertyValueAction) { double newValue = value; // Calculate the new value as increment/decrement from the current value if (propertyValueAction == PropertyValueAction.IncreaseByAbsoluteValue) { newValue = currentValue + value; } else if (propertyValueAction == PropertyValueAction.DecreaseByAbsoluteValue) { newValue = currentValue - value; } // Check limiting boundaries if (newValue < TextEditorCharacters.OneFontPoint) { newValue = TextEditorCharacters.OneFontPoint; } else if (newValue > TextEditorCharacters.MaxFontPoint) { newValue = TextEditorCharacters.MaxFontPoint; } return newValue; } // Helper to set a structural Inline property to a range between start and end positions. private static void SetStructuralInlineProperty(TextPointer start, TextPointer end, DependencyProperty formattingProperty, object value) { DependencyObject commonAncestor = TextPointer.GetCommonAncestor(start, end); ValidateApplyStructuralInlineProperty(start, end, commonAncestor, formattingProperty); if (commonAncestor is Run) { ApplyStructuralInlinePropertyAcrossRun(start, end, (Run)commonAncestor, formattingProperty, value); } else if ((commonAncestor is Inline && !(commonAncestor is AnchoredBlock)) || commonAncestor is Paragraph) { // Even though we don't test for it explicitly, we // should never see InlineUIContainers here because start/end // are always normalized and the inner edges of InlineUIContainer // are not insertion positions. Invariant.Assert(!(commonAncestor is InlineUIContainer)); ApplyStructuralInlinePropertyAcrossInline(start, end, (TextElement)commonAncestor, formattingProperty, value); } else { ApplyStructuralInlinePropertyAcrossParagraphs(start, end, formattingProperty, value); } } private static void FixupStructuralPropertyEnvironment(Inline inline, DependencyProperty property) { // Clear property on parent Spans. ClearParentStructuralPropertyValue(inline, property); // Flatten property on previous Inlines. for (Inline searchInline = inline; searchInline != null; searchInline = searchInline.Parent as Span) { Inline previousSibling = (Inline)searchInline.PreviousElement; if (previousSibling != null) { FlattenStructuralProperties(previousSibling); break; } } // Flatten property on following Inlines. for (Inline searchInline = inline; searchInline != null; searchInline = searchInline.Parent as Span) { Inline nextSibling = (Inline)searchInline.NextElement; if (nextSibling != null) { FlattenStructuralProperties(nextSibling); break; } } } private static void FlattenStructuralProperties(Inline inline) { // Find the topmost Span covering this inline and only other direct ancestors. Span topmostSpan = inline as Span; Span parent = inline.Parent as Span; while (parent != null && parent.Inlines.FirstInline == parent.Inlines.LastInline) { topmostSpan = parent; parent = parent.Parent as Span; } // Push structural properties downward. while (topmostSpan != null && topmostSpan.Inlines.FirstInline == topmostSpan.Inlines.LastInline) { Inline child = (Inline)topmostSpan.Inlines.FirstInline; TransferStructuralProperties(topmostSpan, child); // If there are no more local values on the parent, remove it. if (TextSchema.IsMergeableInline(topmostSpan.GetType()) && TextSchema.IsKnownType(topmostSpan.GetType()) && !HasWriteableLocalPropertyValues(topmostSpan)) { topmostSpan.Reposition(null, null); } topmostSpan = child as Span; } } private static void ClearParentStructuralPropertyValue(Inline child, DependencyProperty property) { // Find the most distant ancestor with a local property value. Span conflictingParent = null; for (Span parent = child.Parent as Span; parent != null && TextSchema.IsMergeableInline(parent.GetType()); parent = parent.Parent as Span) { if (HasLocalPropertyValue(parent, property)) { conflictingParent = parent; } } // Split down from conflictingParent, clearing property values along the way. if (conflictingParent != null) { TextElement limit = (TextElement)conflictingParent.Parent; SplitFormattingElements(child.ElementStart, /*keepEmptyFormatting*/false, limit); TextPointer end = SplitFormattingElements(child.ElementEnd, /*keepEmptyFormatting*/false, limit); Span parent = (Span)end.GetAdjacentElement(LogicalDirection.Backward); while (parent != null && parent != child) { parent.ClearValue(property); Span nextSpan = parent.Inlines.FirstInline as Span; // If there are no more local values on the parent, remove it. if (!HasWriteableLocalPropertyValues(parent)) { // parent.Reposition(null, null); } parent = nextSpan; } } } // Finds a Run element with ElementStart at or after the given pointer // Creates Runs at potential run positions if encounters some. private static Run GetNextRun(TextPointer pointer, TextPointer limit) { Run run = null; while (pointer != null && pointer.CompareTo(limit) < 0) { if (pointer.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementStart && (run = pointer.GetAdjacentElement(LogicalDirection.Forward) as Run) != null) { break; } if (TextPointerBase.IsAtPotentialRunPosition(pointer)) { pointer = TextRangeEditTables.EnsureInsertionPosition(pointer); Invariant.Assert(pointer.Parent is Run); run = pointer.Parent as Run; break; } // Advance the scanning pointer pointer = pointer.GetNextContextPosition(LogicalDirection.Forward); } return run; } // Helper that walks Run and Span elements between start and end positions, // clearing value of passed formattingProperty on them. // private static void ClearPropertyValueFromSpansAndRuns(TextPointer start, TextPointer end, DependencyProperty formattingProperty) { // Normalize start position forward. start = start.GetPositionAtOffset(0, LogicalDirection.Forward); // Move to next context position before entering loop below, // since in the loop we look backward. start = start.GetNextContextPosition(LogicalDirection.Forward); while (start != null && start.CompareTo(end) < 0) { if (start.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && TextSchema.IsFormattingType(start.Parent.GetType())) // look for Run/Span elements { start.Parent.ClearValue(formattingProperty); // Remove unnecessary Spans around this position, delete empty formatting elements (if any) // and merge with adjacent inlines if they have identical set of formatting properties. MergeFormattingInlines(start); } start = start.GetNextContextPosition(LogicalDirection.Forward); } } private static void ApplyStructuralInlinePropertyAcrossRun(TextPointer start, TextPointer end, Run run, DependencyProperty formattingProperty, object value) { if (start.CompareTo(end) == 0) { // When the range is empty we should ignore the command, except // for the case of empty Run which can be encountered in empty paragraphs if (run.IsEmpty) { run.SetValue(formattingProperty, value); } } else { // Split elements at start and end boundaries. start = SplitFormattingElements(start, /*keepEmptyFormatting:*/false, /*limitingAncestor*/run.Parent as TextElement); end = SplitFormattingElements(end, /*keepEmptyFormatting:*/false, /*limitingAncestor*/run.Parent as TextElement); run = (Run)start.GetAdjacentElement(LogicalDirection.Forward); run.SetValue(formattingProperty, value); } // Clear property value from all ancestors of this Run. FixupStructuralPropertyEnvironment(run, formattingProperty); } private static void ApplyStructuralInlinePropertyAcrossInline(TextPointer start, TextPointer end, TextElement commonAncestor, DependencyProperty formattingProperty, object value) { start = SplitFormattingElements(start, /*keepEmptyFormatting:*/false, commonAncestor); end = SplitFormattingElements(end, /*keepEmptyFormatting:*/false, commonAncestor); DependencyObject forwardElement = start.GetAdjacentElement(LogicalDirection.Forward); DependencyObject backwardElement = end.GetAdjacentElement(LogicalDirection.Backward); if (forwardElement == backwardElement && (forwardElement is Run || forwardElement is Span)) { // After splitting we have exactly one Run or Span between start and end. Use it for setting the property. Inline inline = (Inline)start.GetAdjacentElement(LogicalDirection.Forward); // Set the property to existing element. inline.SetValue(formattingProperty, value); // Clear property value from all ancestors of this inline. FixupStructuralPropertyEnvironment(inline, formattingProperty); if (forwardElement is Span) { // Clear property value from all Span and Run children of this span. ClearPropertyValueFromSpansAndRuns(inline.ContentStart, inline.ContentEnd, formattingProperty); } } else { Span span; if (commonAncestor is Span && start.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && end.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd && start.GetAdjacentElement(LogicalDirection.Backward) == commonAncestor) { // Special case when start and end are at parent Span boundaries. // Don't need to create a new Span in this case. span = (Span)commonAncestor; } else { // Create a new span from start to end. span = new Span(); span.Reposition(start, end); } // Set property on the span. span.SetValue(formattingProperty, value); // Clear property value from all ancestors of this span. FixupStructuralPropertyEnvironment(span, formattingProperty); // Clear property value from all Span and Run children of this span. ClearPropertyValueFromSpansAndRuns(span.ContentStart, span.ContentEnd, formattingProperty); } } // Helper that walks paragraphs between start and end positions, applying passed formattingProperty value on them. private static void ApplyStructuralInlinePropertyAcrossParagraphs(TextPointer start, TextPointer end, DependencyProperty formattingProperty, object value) { // We assume to call this method only for paragraph crossing case Invariant.Assert(start.Paragraph != null); Invariant.Assert(start.Paragraph.ContentEnd.CompareTo(end) < 0); // Apply to first Paragraph SetStructuralInlineProperty(start, start.Paragraph.ContentEnd, formattingProperty, value); start = start.Paragraph.ElementEnd; // Apply to last paragraph if (end.Paragraph != null) { SetStructuralInlineProperty(end.Paragraph.ContentStart, end, formattingProperty, value); end = end.Paragraph.ElementStart; } // Now, loop through paragraphs between start and end positions while (start != null && start.CompareTo(end) < 0) { if (start.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && start.Parent is Paragraph) { Paragraph paragraph = (Paragraph)start.Parent; // Apply property to paragraph just found. SetStructuralInlineProperty(paragraph.ContentStart, paragraph.ContentEnd, formattingProperty, value); // Jump to Paragraph end to skip Inline formatting tags. start = paragraph.ElementEnd; } start = start.GetNextContextPosition(LogicalDirection.Forward); } } // Returns false if calling ApplyStructuralInlineProperty will throw an InvalidOperationException with the // same input parameters. // // If property != null, this method will throw an InvalidOperation exception instead of returning false. private static bool ValidateApplyStructuralInlineProperty(TextPointer start, TextPointer end, DependencyObject commonAncestor, DependencyProperty property) { if (!(commonAncestor is Inline)) { return true; } Inline nonMergeableAncestor = null; Inline parent; // Find the first non-mergeable Inline scoping start. for (parent = (Inline)start.Parent; parent != commonAncestor; parent = (Inline)parent.Parent) { if (!TextSchema.IsMergeableInline(parent.GetType())) { nonMergeableAncestor = parent; commonAncestor = parent; break; } } // Try to reach the start non-mergeable or original commonAncestor from end. for (parent = (Inline)end.Parent; parent != commonAncestor; parent = (Inline)parent.Parent) { if (!TextSchema.IsMergeableInline(parent.GetType())) { nonMergeableAncestor = parent; break; } } if (property != null && parent != commonAncestor) { throw new InvalidOperationException(SR.Get(SRID.TextRangeEdit_InvalidStructuralPropertyApply, property, nonMergeableAncestor)); } return (parent == commonAncestor); } #endregion Private Methods } } // File provided for Reference Use Only by Microsoft Corporation (c) 2007. // Copyright (c) Microsoft Corporation. All rights reserved.
Link Menu
This book is available now!
Buy at Amazon US or
Buy at Amazon UK
- FrameworkElement.cs
- MetadataItemCollectionFactory.cs
- ScriptingWebServicesSectionGroup.cs
- WebConfigurationFileMap.cs
- DependencyPropertyDescriptor.cs
- TouchFrameEventArgs.cs
- DeviceContext.cs
- OperationCanceledException.cs
- LogReservationCollection.cs
- ThreadStartException.cs
- StreamWriter.cs
- MergeFilterQuery.cs
- DocumentSequenceHighlightLayer.cs
- SafeNativeMethods.cs
- NavigateUrlConverter.cs
- SqlInfoMessageEvent.cs
- StatusBarDrawItemEvent.cs
- CompressStream.cs
- StringCollection.cs
- PreProcessor.cs
- WmpBitmapDecoder.cs
- SBCSCodePageEncoding.cs
- Attributes.cs
- LinearGradientBrush.cs
- ResourceSet.cs
- HttpListenerContext.cs
- FlowLayoutPanelDesigner.cs
- ReadOnlyCollection.cs
- OpCodes.cs
- PageCatalogPart.cs
- PackageRelationshipSelector.cs
- WebConfigurationHostFileChange.cs
- SortQuery.cs
- PersistenceProvider.cs
- ObjectDataSourceFilteringEventArgs.cs
- SiteIdentityPermission.cs
- InputMethodStateChangeEventArgs.cs
- NumericExpr.cs
- GrammarBuilder.cs
- LabelDesigner.cs
- InheritanceContextHelper.cs
- AVElementHelper.cs
- followingsibling.cs
- FaultCode.cs
- EditingMode.cs
- SiteMembershipCondition.cs
- ComponentRenameEvent.cs
- SerializationHelper.cs
- WindowsRegion.cs
- AdapterUtil.cs
- HtmlTextArea.cs
- TemplatedWizardStep.cs
- LoadWorkflowCommand.cs
- CallbackValidatorAttribute.cs
- RowParagraph.cs
- Color.cs
- XPathDescendantIterator.cs
- InvokeGenerator.cs
- TypeSystem.cs
- CaseExpr.cs
- WebPartDisplayMode.cs
- Int32AnimationUsingKeyFrames.cs
- MailMessageEventArgs.cs
- RawStylusSystemGestureInputReport.cs
- COM2EnumConverter.cs
- IPGlobalProperties.cs
- ListViewItemMouseHoverEvent.cs
- TagPrefixInfo.cs
- COM2FontConverter.cs
- ContentValidator.cs
- PropertyTabChangedEvent.cs
- BulletedList.cs
- CombinedGeometry.cs
- CacheSection.cs
- DataGridParentRows.cs
- PassportAuthenticationModule.cs
- MimeWriter.cs
- TypeToTreeConverter.cs
- Color.cs
- RecognizeCompletedEventArgs.cs
- DependencyPropertyAttribute.cs
- CompModSwitches.cs
- NamedObject.cs
- DataContractSet.cs
- DefaultSerializationProviderAttribute.cs
- TypeUsageBuilder.cs
- TypeDelegator.cs
- TlsnegoTokenProvider.cs
- RenderDataDrawingContext.cs
- ShapingEngine.cs
- PageTheme.cs
- TabItemWrapperAutomationPeer.cs
- StructuredTypeEmitter.cs
- WebPartDescription.cs
- Rect3D.cs
- IntSecurity.cs
- SupportingTokenDuplexChannel.cs
- CacheDependency.cs
- ColumnWidthChangedEvent.cs
- RuntimeCompatibilityAttribute.cs