|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using MS.Internal;
using MS.Internal.PtsHost.UnsafeNativeMethods; // PTS restrictions to obtain TextIndent valid value range.
//
// Description: Static internal class providing a set of
// helpoer methods for text editing operations
//
namespace System.Windows.Documents
{
/// <summary>
/// The TextRange class represents a pair of TextPositions, with many
/// rich text editing operations exposed.
/// </summary>
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;
}
/// <summary>
/// Checks if scoping element is empty formatting.
/// It must be removed if not situated inside of empty block.
/// </summary>
/// <param name="position">
/// TextPointer scoped by the allegedly empty formatting element(s).
/// </param>
/// <returns>
/// true if at least one empty formatting element was extracted.
/// </returns>
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;
}
/// <summary>
/// Applies a property to a range between start and end positions.
/// </summary>
/// <param name="start">
/// TextPointer identifying start of affected range.
/// </param>
/// <param name="end">
/// TextPointer identifying end of affected range.
/// </param>
/// <param name="formattingProperty">
/// A dependency property whose value is supposed to applied to a range.
/// </param>
/// <param name="value">
/// A value for a property to apply.
/// </param>
/// <param name="propertyValueAction">
/// Specifies how to use the value - as absolute, as increment or a decrement.
/// </param>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="position">
/// 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).
/// </param>
/// <param name="moveIntoSecondParagraph">
/// 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).
/// </param>
/// <remarks>
/// This function could be implemented from TextContainer class.
/// </remarks>
/// <returns>
/// 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.
/// </returns>
internal static TextPointer InsertParagraphBreak(TextPointer position, bool moveIntoSecondParagraph)
{
Invariant.Assert(position.TextContainer.Parent == null || 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;
if (paragraph == null)
{
// At this point, we expect we're working in a fragment of Inlines only.
Invariant.Assert(position.TextContainer.Parent == null);
// Add a parent Paragraph to split.
paragraph = new Paragraph();
paragraph.Reposition(position.DocumentStart, position.DocumentEnd);
}
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="position">
/// </param>
/// <returns>
/// TextPointer positioned in the beginning of a Run immediately following a LineBreak inserted.
/// </returns>
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 to insert a LineBreak element");
LineBreak lineBreak = new LineBreak();
position.InsertTextElement(lineBreak);
return lineBreak.ElementEnd.GetInsertionPosition(LogicalDirection.Forward);
}
/// <summary>
/// Applies formatting properties for whole block elements.
/// </summary>
/// <param name="start">
/// a position within first block in sequence
/// </param>
/// <param name="end">
/// a positionn within last block in sequence
/// </param>
/// <param name="property">
/// property changed on blocks
/// </param>
/// <param name="value">
/// value for the property
/// </param>
internal static void SetParagraphProperty(TextPointer start, TextPointer end, DependencyProperty property, object value)
{
SetParagraphProperty(start, end, property, value, PropertyValueAction.SetValue);
}
/// <summary>
/// Applies formatting properties for whole block elements.
/// </summary>
/// <param name="start">
/// a position within first block in sequence
/// </param>
/// <param name="end">
/// a positionn within last block in sequence
/// </param>
/// <param name="property">
/// property changed on blocks
/// </param>
/// <param name="value">
/// value for the property
/// </param>
/// <param name="propertyValueAction">
/// Specifies how to use the value - as absolute, as increment or a decrement.
/// </param>
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)
{
object currentValue = block.GetValue(property);
// Set FlowDirection property on List
SetPropertyValue(block, property, currentValue:currentValue, newValue:value);
// Only swap Left and Right margins of the list when FlowDirection changes. This ensures indentation is mirrored correctly.
if (!Object.Equals(currentValue, value))
{
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 = GetNewDoubleValue(property, (double)currentValue, (double)newValue, propertyValueAction);
}
SetPropertyValue(block, property, currentValue, newValue);
if (property == Block.FlowDirectionProperty)
{
// Only swap Left and Right margins of the paragraph when FlowDirection changes. This ensures indentation is mirrored correctly.
if (!Object.Equals(currentValue, newValue))
{
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
: GetNewDoubleValue(null, currentThickness.Top, newThickness.Top, propertyValueAction);
double bottomMargin = newThickness.Bottom < 0
? currentThickness.Bottom
: GetNewDoubleValue(null, 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
: GetNewDoubleValue(null, currentThickness.Left, newThickness.Right, propertyValueAction);
rightMargin = newThickness.Left < 0
? currentThickness.Right
: GetNewDoubleValue(null, currentThickness.Right, newThickness.Left, propertyValueAction);
}
else
{
leftMargin = newThickness.Left < 0
? currentThickness.Left
: GetNewDoubleValue(null, currentThickness.Left, newThickness.Left, propertyValueAction);
rightMargin = newThickness.Right < 0
? currentThickness.Right
: GetNewDoubleValue(null, 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;
}
/// <summary>
/// Calculates valid value for specified DP, current and new (desired) value,
/// and <see cref="PropertyValueAction"/>.
/// The value is made to adhere editor's acceptable range of values for given property.
/// If the value is invalid, then closest valid bound of the range is returned.
/// </summary>
/// <param name="property"></param>
/// <param name="currentValue"></param>
/// <param name="newValue"></param>
/// <param name="propertyValueAction"></param>
/// <returns>new value</returns>
private static double GetNewDoubleValue(DependencyProperty property, double currentValue, double newValue, PropertyValueAction propertyValueAction)
{
double outValue = NewValue(currentValue, newValue, propertyValueAction);
return DoublePropertyBounds.GetClosestValidValue(property, outValue);
}
// 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;
}
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;
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
//
// .....................................................................
/// <summary>
/// Increments/decrements paragraph leading maring property.
/// For LeftToRight paragraphs a leading maring is the left marinng,
/// for RightToLeft paragraphs it is the right maring.
/// </summary>
/// <param name="range"></param>
/// <param name="increment"></param>
/// <param name="propertyValueAction">
/// Must be one of IncreaseValue or DecreaseValue.
/// </param>
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);
}
/// <summary>
/// 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).
/// </summary>
/// <param name="start"></param>
/// <param name="end"></param>
internal static void DeleteInlineContent(ITextPointer start, ITextPointer end)
{
DeleteParagraphContent(start, end);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="start">
/// Position indicating a beginning of deleted content.
/// </param>
/// <param name="end">
/// Position indicating an end of deleted content.
/// </param>
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)
{
// This code is incorrect as it can delete last paragraph from TableCell, Section, TextFlow, ... or last Inline from a paragraph
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);
}
// Anything required for custom types here?
}
// 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;
}
}
}
/// <summary>
/// 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).
/// </summary>
/// <param name="start">
/// A position from which content clearinng starts.
/// All content segments between this position and a fragment
/// root will be deleted.
/// </param>
/// <param name="end">
/// A position indicating the other boundary of a fragment.
/// This position is used for fragment root identification.
/// </param>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="one">
/// One of content boundary positions. May precede or follow the TextPointer two.
/// Must belong to the same scope as TextPointer two.
/// </param>
/// <param name="two">
/// Another content boundary position. May precede or follow the TextPointer one.
/// Must belong to the same scope as TextPointer one.
/// </param>
/// <returns>
/// true if surrounding formatting elements have beed deleted as a side effect.
/// </returns>
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);
}
/// <summary>
/// Splits all inline element walking up to specified limitingAncestor.
/// limitingAncestor remains unsplit.
/// </summary>
/// <param name="splitPosition">
/// Position at which splitting happens. After the operation the position
/// is between split elements - scoped by limitingElement (if it is not frozen).
/// </param>
/// <param name="keepEmptyFormatting">
/// Flag to indicate whether split operation should create empty formatting tags.
/// </param>
/// <param name="preserveStructuralFormatting">
/// 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).
/// </param>
/// <param name="limitingAncestor">
/// If null, this has no impact on split operation.
/// Otherwise, this method ensures that this ancestor boundary is not crossed while splitting.
/// </param>
/// <returns>
/// 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.
/// </returns>
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 &&
((run.Parent != null && HasLocalInheritableStructuralPropertyValue(run)) ||
(run.Parent == null && HasLocalStructuralPropertyValue(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 ||
((((Inline)splitPosition.Parent).Parent != null && !HasLocalInheritableStructuralPropertyValue((Inline)splitPosition.Parent)) ||
(((Inline)splitPosition.Parent).Parent == null && !HasLocalStructuralPropertyValue((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))
{
// we could try to merge character properties here as well, when parent
// Spans are removed. The split calls above may have fragmented other Spans
// unnecessarily now that we're removing a scoping Span.
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.
// REVIEW:benwest:5/4/2006: shouldn't this just clear top-level elements?
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.Format(SR.TextRangeEdit_InvalidStructuralPropertyApply, property, nonMergeableAncestor));
}
return (parent == commonAncestor);
}
#endregion Private Methods
#region Private Types
/// <summary>
/// This class imposes value ranges, considered valid by editing code, for Dependency properties of type double.
/// In other words this class defines value range policies for DPs of type double, in editing context.
/// </summary>
internal static class DoublePropertyBounds
{
/// <summary>
/// Validates the value and if it's in permitable range then the <paramref name="value"/> is returned.
/// Oterwise closest bound(lower/upper) of the range is returned.
/// </summary>
/// <param name="property"></param>
/// <param name="value"></param>
/// <returns></returns>
internal static double GetClosestValidValue(DependencyProperty property, double value)
{
DoublePropertyRange valueRange = GetValueRange(property);
return valueRange.GetClosestValue(value);
}
/// <summary>
/// Returns the acceptable range of values for given property.
/// if <paramref name="property"/> is null, or there is no value range specified for given property,
/// then <see cref="DefaultRange "/> is returned.
/// </summary>
/// <param name="property"></param>
/// <returns></returns>
private static DoublePropertyRange GetValueRange(DependencyProperty property)
{
for (int i = 0; i < _ranges.Length; i++)
{
if (property == _ranges[i].Property)
{
return _ranges[i];
}
}
return DefaultRange;
}
/// <summary>
/// Range for properties whcih do not have explicit specification of the acceptable value ranges.
/// </summary>
private static DoublePropertyRange DefaultRange
{
get { return _ranges[0]; }
}
static readonly DoublePropertyRange[] _ranges = new DoublePropertyRange[]
{
// 1st entry is the default value range for properties not having explicit ranges specified here.
new DoublePropertyRange(null, 0, double.MaxValue),
new DoublePropertyRange (Paragraph.TextIndentProperty, -Math.Min(1000000, PTS.MaxPageSize), Math.Min(1000000, PTS.MaxPageSize))
};
/// <summary>
/// Range of <see cref="double"/> values for a given <see cref="DependencyProperty"/>.
/// </summary>
private struct DoublePropertyRange
{
internal DoublePropertyRange(DependencyProperty property, double lowerBound, double upperBound)
{
Invariant.Assert(lowerBound < upperBound);
_lowerBound = lowerBound;
_upperBound = upperBound;
_property = property;
}
/// <summary>
/// Returns <paramref name="value"/> if it is in range, or returns the closest boundary.
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
internal double GetClosestValue(double value)
{
double retValue = Math.Max(_lowerBound, value);
retValue = Math.Min(retValue, _upperBound);
return retValue;
}
internal DependencyProperty Property { get { return _property; } }
private DependencyProperty _property;
private double _lowerBound;
private double _upperBound;
}
}
#endregion Private Types
}
}
|