|
//---------------------------------------------------------------------------
//
// Copyright (C) Microsoft Corporation. All rights reserved.
//
// File: TextBlock.cs
//
// Description: TextBlock element displays text. It is meant for UI scenarios.
// Most text scenarios should use the FlowDocumentScrollViewer.
//
// History:
// 04/25/2003 : Microsoft - created.
//
//---------------------------------------------------------------------------
using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using System.Globalization;
using System.Windows.Automation;
using System.Windows.Threading;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
using System.Windows.Markup;
using MS.Utility;
using MS.Internal; // Invariant.Assert
using MS.Internal.Automation; // TextAdaptor
using System.Windows.Automation.Peers;
using MS.Internal.Text;
using MS.Internal.Documents;
using MS.Internal.Controls;
using MS.Internal.PresentationFramework;
using MS.Internal.Telemetry.PresentationFramework;
#pragma warning disable 1634, 1691 // suppressing PreSharp warnings
namespace System.Windows.Controls
{
/// <summary>
/// TextBlockCache caches the properties and Line which can be
/// reused during Measure, Arrange and Render phase.
/// </summary>
///
class TextBlockCache
{
public LineProperties _lineProperties;
public TextRunCache _textRunCache;
}
/// <summary>
/// TextBlock element displays text. It is meant for UI scenarios.
/// Most text scenarios should use the FlowDocumentScrollViewer.
/// </summary>
[ContentProperty("Inlines")]
[Localizability(LocalizationCategory.Text)]
public class TextBlock : FrameworkElement, IContentHost, IAddChildInternal, IServiceProvider
{
//-------------------------------------------------------------------
//
// IContentHost Members
//
//-------------------------------------------------------------------
#region IContentHost Members
/// <summary>
/// Hit tests to the correct ContentElement within the ContentHost
/// that the mouse is over.
/// </summary>
/// <param name="point">Mouse coordinates relative to the ContentHost.</param>
IInputElement IContentHost.InputHitTest(Point point)
{
return this.InputHitTestCore(point);
}
/// <summary>
/// Returns an ICollection of bounding rectangles for the given ContentElement
/// </summary>
/// <param name="child">
/// Content element for which rectangles are required
/// </param>
/// <remarks>
/// Looks at the ContentElement e line by line and gets rectangle bounds for each line
/// </remarks>
ReadOnlyCollection<Rect> IContentHost.GetRectangles(ContentElement child)
{
return this.GetRectanglesCore(child);
}
/// <summary>
/// Returns elements hosted by the content host as an enumerator class
/// </summary>
IEnumerator<IInputElement> IContentHost.HostedElements
{
get
{
return this.HostedElementsCore;
}
}
/// <summary>
/// Called when a UIElement-derived class which is hosted by a IContentHost changes its DesiredSize
/// NOTE: This method already exists for this class and is not specially implemented for IContentHost.
/// If this method is called through IContentHost for this class it will fire an assert
/// </summary>
/// <param name="child">
/// Child element whose DesiredSize has changed
/// </param>
void IContentHost.OnChildDesiredSizeChanged(UIElement child)
{
this.OnChildDesiredSizeChangedCore(child);
}
#endregion IContentHost Members
//-------------------------------------------------------------------
//
// IContentHost Members
//
//-------------------------------------------------------------------
#region IAddChild members
///<summary>
/// Called to Add the object as a Child.
///</summary>
///<param name="value">
/// Object to add as a child
///</param>
void IAddChild.AddChild(Object value)
{
if (value == null)
{
throw new ArgumentNullException("value");
}
EnsureComplexContent();
if (!(_complexContent.TextContainer is TextContainer))
{
throw new ArgumentException(SR.Get(SRID.TextPanelIllegalParaTypeForIAddChild, "value", value.GetType()));
}
// Get parent of the text container. Note that it can be not a "this" TextBlock - in case
// when a TextBlock is used as a storage for plain TextBox - as an owner of a text container.
Type parentType = _complexContent.TextContainer.Parent.GetType();
Type valueType = value.GetType();
// Do implicit conversion to allowed inline type - if possible
if (!TextSchema.IsValidChildOfContainer(parentType, /*childType*/valueType))
{
if (value is UIElement)
{
value = new InlineUIContainer((UIElement)value);
}
else
{
throw new ArgumentException(SR.Get(SRID.TextSchema_ChildTypeIsInvalid, parentType.Name, valueType.Name));
}
}
// Insert inline element into the text container
Invariant.Assert(value is Inline, "Schema validation helper must guarantee that invalid element is not passed here");
TextContainer textContainer = (TextContainer)_complexContent.TextContainer;
textContainer.BeginChange();
try
{
TextPointer endPosition = textContainer.End;
textContainer.InsertElementInternal(endPosition, endPosition, (Inline)value);
}
finally
{
textContainer.EndChange();
}
}
///<summary>
/// Called when text appears under the tag in markup.
///</summary>
///<param name="text">
/// Text to Add to the Object
///</param>
void IAddChild.AddText(string text)
{
if (text == null)
{
throw new ArgumentNullException("text");
}
if (_complexContent == null)
{
Text = Text + text;
}
else
{
TextContainer textContainer = (TextContainer)_complexContent.TextContainer;
textContainer.BeginChange();
try
{
TextPointer endPosition = textContainer.End;
Run implicitRun = Inline.CreateImplicitRun(this);
textContainer.InsertElementInternal(endPosition, endPosition, implicitRun);
implicitRun.Text = text;
}
finally
{
textContainer.EndChange();
}
}
}
#endregion IAddChild members
//-------------------------------------------------------------------
//
// LogicalTree
//
//-------------------------------------------------------------------
#region LogicalTree
/// <summary>
/// Returns enumerator to logical children.
/// </summary>
protected internal override IEnumerator LogicalChildren
{
get
{
if (IsContentPresenterContainer)
{
// We are hosting content that belongs to a ContentPresenter
return EmptyEnumerator.Instance;
}
else if (_complexContent == null)
{
return new SimpleContentEnumerator(Text);
}
else
{
if (!_complexContent.ForeignTextContainer)
{
return new RangeContentEnumerator(this.ContentStart, this.ContentEnd);
}
else
{
return EmptyEnumerator.Instance; // When we do not own the text tree we may not report its children as ours.
}
}
}
}
#endregion LogicalTree
//-------------------------------------------------------------------
//
// IServiceProvider Members
//
//-------------------------------------------------------------------
#region IServiceProvider Members
/// <summary>
/// Gets the service object of the specified type.
/// </summary>
/// <remarks>
/// TextBlock control currently supports only TextView and TextContainer.
/// </remarks>
/// <param name="serviceType">
/// An object that specifies the type of service object to get.
/// </param>
/// <returns>
/// A service object of type serviceType. A null reference if there is no
/// service object of type serviceType.
/// </returns>
object IServiceProvider.GetService(Type serviceType)
{
if (serviceType == null)
{
throw new ArgumentNullException("serviceType");
}
// VerifyAccess();
if (serviceType == typeof(ITextView))
{
EnsureComplexContent();
return _complexContent.TextView;
}
else if (serviceType == typeof(ITextContainer))
{
EnsureComplexContent();
return _complexContent.TextContainer;
}
else if (serviceType == typeof(TextContainer))
{
EnsureComplexContent();
return _complexContent.TextContainer as TextContainer;
}
return null;
}
#endregion IServiceProvider Members
//-------------------------------------------------------------------
//
// Constructors
//
//-------------------------------------------------------------------
#region Constructors
/// <summary>
/// TextBlock static constructor. Registers metadata for its properties.
/// </summary>
static TextBlock()
{
// Required here, just for this type - Don't want anyone else to pick up this GetValueOverride
BaselineOffsetProperty.OverrideMetadata(
typeof(TextBlock),
new FrameworkPropertyMetadata(
null,
new CoerceValueCallback(CoerceBaselineOffset)));
// Registering typography properties metadata
PropertyChangedCallback onTypographyChanged = new PropertyChangedCallback(OnTypographyChanged);
DependencyProperty[] typographyProperties = Typography.TypographyPropertiesList;
for (int i = 0; i < typographyProperties.Length; i++)
{
typographyProperties[i].OverrideMetadata(typeof(TextBlock), new FrameworkPropertyMetadata(onTypographyChanged));
}
EventManager.RegisterClassHandler(typeof(TextBlock), RequestBringIntoViewEvent, new RequestBringIntoViewEventHandler(OnRequestBringIntoView));
DefaultStyleKeyProperty.OverrideMetadata(typeof(TextBlock), new FrameworkPropertyMetadata(typeof(TextBlock)));
ControlsTraceLogger.AddControl(TelemetryControls.TextBlock);
}
/// <summary>
/// Initializes a new instance of TextBlock class.
/// </summary>
public TextBlock() : base()
{
Initialize();
}
/// <summary>
/// Initializes a new inslace of TextBlock class and adds a first Inline to its Inline collection.
/// </summary>
public TextBlock(Inline inline)
: base()
{
Initialize();
if (inline == null)
{
throw new ArgumentNullException("inline");
}
this.Inlines.Add(inline);
}
private void Initialize()
{
}
#endregion Constructors
//-------------------------------------------------------------------
//
// Public Methods
//
//-------------------------------------------------------------------
#region Public Methods
/// <summary>
/// Returns the TextPointer located closest to a supplied Point.
/// </summary>
/// <param name="point">
/// Point to query, in the coordinate space of the Text.
/// </param>
/// <param name="snapToText">
/// If true, this method will always return a TextPointer --
/// the closest position as calculated by the control's heuristics.
/// If false, this method will return a null position if the test
/// point does not fall within any character bounding box.
/// </param>
/// <returns>
/// The closest TextPointer to the supplied Point, or null if
/// snapToText is false and the supplied Point is not contained
/// within any character bounding box.
/// </returns>
/// <exception cref="System.InvalidOperationException">
/// Throws InvalidOperationException if layout is dirty.
/// </exception>
/// <remarks>
/// The TextPointer returned always has its IsFrozen property set true
/// and LogicalDirection property set towards a character hit by the point,
/// or closest to this point.
/// </remarks>
public TextPointer GetPositionFromPoint(Point point, bool snapToText)
{
TextPointer position;
if(CheckFlags(Flags.ContentChangeInProgress))
{
throw new InvalidOperationException(SR.Get(SRID.TextContainerChangingReentrancyInvalid));
}
EnsureComplexContent();
// Validate layout information on TextView.
if (((ITextView)_complexContent.TextView).Validate(point))
{
position = (TextPointer)_complexContent.TextView.GetTextPositionFromPoint(point, snapToText);
}
else
{
position = snapToText ? new TextPointer((TextPointer)_complexContent.TextContainer.Start) : null;
}
//
return position;
}
#endregion Public Methods
//-------------------------------------------------------------------
//
// Public Properties
//
//-------------------------------------------------------------------
#region Public Properties
/// <value>
/// Collection of Inline items contained in this TextBlock.
/// </value>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public InlineCollection Inlines
{
get
{
return new InlineCollection(this, /*isOwnerParent*/true);
}
}
/// <summary>
/// TextPointer preceding all content.
/// </summary>
/// <remarks>
/// The TextPointer returned always has its IsFrozen property set true
/// and LogicalDirection property set to LogicalDirection.Backward.
/// </remarks>
public TextPointer ContentStart
{
get
{
EnsureComplexContent();
return (TextPointer)_complexContent.TextContainer.Start;
}
}
/// <summary>
/// TextPointer following all content.
/// </summary>
/// <remarks>
/// The TextPointer returned always has its IsFrozen property set true
/// and LogicalDirection property set to LogicalDirection.Forward.
/// </remarks>
public TextPointer ContentEnd
{
get
{
EnsureComplexContent();
return (TextPointer)_complexContent.TextContainer.End;
}
}
/// <value>
/// A TextRange spanning the content of this element.
/// </value>
internal TextRange TextRange
{
get
{
// NOTE: We are creating a new instance of a TextRange on each request.
// We cannot cache the instance, because it may become incorrect
// after collapsing TextBlock's content and insertion a new one:
// the cached range would remain empty, which is incorrect.
return new TextRange(this.ContentStart, this.ContentEnd);
}
}
/// <summary>
/// Breaking condition before the Element.
/// </summary>
public LineBreakCondition BreakBefore { get { return LineBreakCondition.BreakDesired; } }
/// <summary>
/// Breaking condition after the Element.
/// </summary>
public LineBreakCondition BreakAfter { get { return LineBreakCondition.BreakDesired; } }
/// <summary>
/// Access to all text typography properties.
/// </summary>
public Typography Typography
{
get
{
return new Typography(this);
}
}
#endregion Public Properties
//-------------------------------------------------------------------
//
// Public Dynamic Properties
//
//-------------------------------------------------------------------
#region Public Dynamic Properties
/// <summary>
/// DependencyProperty for <see cref="BaselineOffset" /> property.
/// </summary>
public static readonly DependencyProperty BaselineOffsetProperty =
DependencyProperty.RegisterAttached(
"BaselineOffset",
typeof(double),
typeof(TextBlock),
new FrameworkPropertyMetadata(
double.NaN,
new PropertyChangedCallback(OnBaselineOffsetChanged)));
/// <summary>
/// The BaselineOffset property provides an adjustment to baseline offset
/// </summary>
public double BaselineOffset
{
get { return (double)GetValue(BaselineOffsetProperty); }
set { SetValue(BaselineOffsetProperty, value); }
}
/// <summary>
/// Writes the attached property BaselineOffset to the given element.
/// </summary>
/// <param name="element">The element to which to write the attached property.</param>
/// <param name="value">The property value to set</param>
public static void SetBaselineOffset(DependencyObject element, double value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(BaselineOffsetProperty, value);
}
/// <summary>
/// Reads the attached property from the given element
/// </summary>
/// <param name="element">The element to which to read the attached property.</param>
public static double GetBaselineOffset(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (double)element.GetValue(BaselineOffsetProperty);
}
/// <summary>
/// DependencyProperty for <see cref="Text" /> property.
/// </summary>
[CommonDependencyProperty]
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(
"Text",
typeof(string),
typeof(TextBlock),
new FrameworkPropertyMetadata(
string.Empty,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
new PropertyChangedCallback(OnTextChanged),
new CoerceValueCallback(CoerceText)));
/// <summary>
/// The Text property defines the content (text) to be displayed.
/// </summary>
[Localizability(LocalizationCategory.Text)]
public string Text
{
get { return (string) GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
/// <summary>
/// Coersion callback for the Text property.
/// </summary>
/// <remarks>
/// We cannot assume value is a string here -- it may be a DeferredTextReference.
/// </remarks>
private static object CoerceText(DependencyObject d, object value)
{
TextBlock textblock = (TextBlock)d;
if (value == null)
{
value = String.Empty;
}
if (textblock._complexContent != null &&
!textblock.CheckFlags(Flags.TextContentChanging) &&
(string)value == (string)textblock.GetValue(TextProperty))
{
// If the new value equals the old value, then the property
// system will optimize out the call to OnTextChanged. We can't
// skip this call because there's ambiguity between the TextProperty
// view of content and actual content -- we might have a new
// value even if strings match.
//
// E.g.: content = <Image/>, TextProperty == " "
// Now setting TextProperty = " " really changes content, replacing
// the Image with a space char.
OnTextChanged(d, (string)value);
}
return value;
}
/// <summary>
/// DependencyProperty for <see cref="FontFamily" /> property.
/// </summary>
[CommonDependencyProperty]
public static readonly DependencyProperty FontFamilyProperty =
TextElement.FontFamilyProperty.AddOwner(typeof(TextBlock));
/// <summary>
/// The FontFamily property specifies the name of font family.
/// </summary>
[Localizability(LocalizationCategory.Font)]
public FontFamily FontFamily
{
get { return (FontFamily) GetValue(FontFamilyProperty); }
set { SetValue(FontFamilyProperty, value); }
}
/// <summary>
/// DependencyProperty setter for <see cref="FontFamily" /> property.
/// </summary>
/// <param name="element">The element to which to write the attached property.</param>
/// <param name="value">The property value to set</param>
public static void SetFontFamily(DependencyObject element, FontFamily value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(FontFamilyProperty, value);
}
/// <summary>
/// DependencyProperty getter for <see cref="FontFamily" /> property.
/// </summary>
/// <param name="element">The element from which to read the attached property.</param>
public static FontFamily GetFontFamily(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (FontFamily)element.GetValue(FontFamilyProperty);
}
/// <summary>
/// DependencyProperty for <see cref="FontStyle" /> property.
/// </summary>
[CommonDependencyProperty]
public static readonly DependencyProperty FontStyleProperty =
TextElement.FontStyleProperty.AddOwner(typeof(TextBlock));
/// <summary>
/// The FontStyle property requests normal, italic, and oblique faces within a font family.
/// </summary>
public FontStyle FontStyle
{
get { return (FontStyle) GetValue(FontStyleProperty); }
set { SetValue(FontStyleProperty, value); }
}
/// <summary>
/// DependencyProperty setter for <see cref="FontStyle" /> property.
/// </summary>
/// <param name="element">The element to which to write the attached property.</param>
/// <param name="value">The property value to set</param>
public static void SetFontStyle(DependencyObject element, FontStyle value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(FontStyleProperty, value);
}
/// <summary>
/// DependencyProperty getter for <see cref="FontStyle" /> property.
/// </summary>
/// <param name="element">The element from which to read the attached property.</param>
public static FontStyle GetFontStyle(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (FontStyle)element.GetValue(FontStyleProperty);
}
/// <summary>
/// DependencyProperty for <see cref="FontWeight" /> property.
/// </summary>
[CommonDependencyProperty]
public static readonly DependencyProperty FontWeightProperty =
TextElement.FontWeightProperty.AddOwner(typeof(TextBlock));
/// <summary>
/// The FontWeight property specifies the weight of the font.
/// </summary>
public FontWeight FontWeight
{
get { return (FontWeight) GetValue(FontWeightProperty); }
set { SetValue(FontWeightProperty, value); }
}
/// <summary>
/// DependencyProperty setter for <see cref="FontWeight" /> property.
/// </summary>
/// <param name="element">The element to which to write the attached property.</param>
/// <param name="value">The property value to set</param>
public static void SetFontWeight(DependencyObject element, FontWeight value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(FontWeightProperty, value);
}
/// <summary>
/// DependencyProperty getter for <see cref="FontWeight" /> property.
/// </summary>
/// <param name="element">The element from which to read the attached property.</param>
public static FontWeight GetFontWeight(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (FontWeight)element.GetValue(FontWeightProperty);
}
/// <summary>
/// DependencyProperty for <see cref="FontStretch" /> property.
/// </summary>
[CommonDependencyProperty]
public static readonly DependencyProperty FontStretchProperty =
TextElement.FontStretchProperty.AddOwner(typeof(TextBlock));
/// <summary>
/// The FontStretch property selects a normal, condensed, or extended face from a font family.
/// </summary>
public FontStretch FontStretch
{
get { return (FontStretch) GetValue(FontStretchProperty); }
set { SetValue(FontStretchProperty, value); }
}
/// <summary>
/// DependencyProperty setter for <see cref="FontStretch" /> property.
/// </summary>
/// <param name="element">The element to which to write the attached property.</param>
/// <param name="value">The property value to set</param>
public static void SetFontStretch(DependencyObject element, FontStretch value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(FontStretchProperty, value);
}
/// <summary>
/// DependencyProperty getter for <see cref="FontStretch" /> property.
/// </summary>
/// <param name="element">The element from which to read the attached property.</param>
public static FontStretch GetFontStretch(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (FontStretch)element.GetValue(FontStretchProperty);
}
/// <summary>
/// DependencyProperty for <see cref="FontSize" /> property.
/// </summary>
[CommonDependencyProperty]
public static readonly DependencyProperty FontSizeProperty =
TextElement.FontSizeProperty.AddOwner(
typeof(TextBlock));
/// <summary>
/// The FontSize property specifies the size of the font.
/// </summary>
[TypeConverter(typeof(FontSizeConverter))]
[Localizability(LocalizationCategory.None)]
public double FontSize
{
get { return (double) GetValue(FontSizeProperty); }
set { SetValue(FontSizeProperty, value); }
}
/// <summary>
/// DependencyProperty setter for <see cref="FontSize" /> property.
/// </summary>
/// <param name="element">The element to which to write the attached property.</param>
/// <param name="value">The property value to set</param>
public static void SetFontSize(DependencyObject element, double value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(FontSizeProperty, value);
}
/// <summary>
/// DependencyProperty getter for <see cref="FontSize" /> property.
/// </summary>
/// <param name="element">The element from which to read the attached property.</param>
[TypeConverter(typeof(FontSizeConverter))]
public static double GetFontSize(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (double)element.GetValue(FontSizeProperty);
}
/// <summary>
/// DependencyProperty for <see cref="Foreground" /> property.
/// </summary>
[CommonDependencyProperty]
public static readonly DependencyProperty ForegroundProperty =
TextElement.ForegroundProperty.AddOwner(
typeof(TextBlock));
/// <summary>
/// The Foreground property specifies the foreground brush of an element's text content.
/// </summary>
public Brush Foreground
{
get { return (Brush) GetValue(ForegroundProperty); }
set { SetValue(ForegroundProperty, value); }
}
/// <summary>
/// DependencyProperty setter for <see cref="Foreground" /> property.
/// </summary>
/// <param name="element">The element to which to write the attached property.</param>
/// <param name="value">The property value to set</param>
public static void SetForeground(DependencyObject element, Brush value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(ForegroundProperty, value);
}
/// <summary>
/// DependencyProperty getter for <see cref="Foreground" /> property.
/// </summary>
/// <param name="element">The element from which to read the attached property.</param>
public static Brush GetForeground(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (Brush)element.GetValue(ForegroundProperty);
}
/// <summary>
/// DependencyProperty for <see cref="Background" /> property.
/// </summary>
[CommonDependencyProperty]
public static readonly DependencyProperty BackgroundProperty =
TextElement.BackgroundProperty.AddOwner(
typeof(TextBlock),
new FrameworkPropertyMetadata(
null,
FrameworkPropertyMetadataOptions.AffectsRender));
/// <summary>
/// The Background property defines the brush used to fill the content area.
/// </summary>
public Brush Background
{
get { return (Brush) GetValue(BackgroundProperty); }
set { SetValue(BackgroundProperty, value); }
}
/// <summary>
/// DependencyProperty for <see cref="TextDecorations" /> property.
/// </summary>
[CommonDependencyProperty]
public static readonly DependencyProperty TextDecorationsProperty =
Inline.TextDecorationsProperty.AddOwner(
typeof(TextBlock),
new FrameworkPropertyMetadata(
new FreezableDefaultValueFactory(TextDecorationCollection.Empty),
FrameworkPropertyMetadataOptions.AffectsRender
));
/// <summary>
/// The TextDecorations property specifies decorations that are added to the text of an element.
/// </summary>
public TextDecorationCollection TextDecorations
{
get { return (TextDecorationCollection) GetValue(TextDecorationsProperty); }
set { SetValue(TextDecorationsProperty, value); }
}
/// <summary>
/// DependencyProperty for <see cref="TextEffects" /> property.
/// </summary>
public static readonly DependencyProperty TextEffectsProperty =
TextElement.TextEffectsProperty.AddOwner(
typeof(TextBlock),
new FrameworkPropertyMetadata(
new FreezableDefaultValueFactory(TextEffectCollection.Empty),
FrameworkPropertyMetadataOptions.AffectsRender));
/// <summary>
/// The TextEffects property specifies effects that are added to the text of an element.
/// </summary>
public TextEffectCollection TextEffects
{
get { return (TextEffectCollection) GetValue(TextEffectsProperty); }
set { SetValue(TextEffectsProperty, value); }
}
/// <summary>
/// DependencyProperty for <see cref="LineHeight" /> property.
/// </summary>
public static readonly DependencyProperty LineHeightProperty =
Block.LineHeightProperty.AddOwner(typeof(TextBlock));
/// <summary>
/// The LineHeight property specifies the height of each generated line box.
/// </summary>
[TypeConverter(typeof(LengthConverter))]
public double LineHeight
{
get { return (double)GetValue(LineHeightProperty); }
set { SetValue(LineHeightProperty, value); }
}
/// <summary>
/// DependencyProperty setter for <see cref="LineHeight" /> property.
/// </summary>
/// <param name="element">The element to which to write the attached property.</param>
/// <param name="value">The property value to set</param>
public static void SetLineHeight(DependencyObject element, double value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(LineHeightProperty, value);
}
/// <summary>
/// DependencyProperty getter for <see cref="LineHeight" /> property.
/// </summary>
/// <param name="element">The element from which to read the attached property.</param>
[TypeConverter(typeof(LengthConverter))]
public static double GetLineHeight(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (double)element.GetValue(LineHeightProperty);
}
/// <summary>
/// DependencyProperty for <see cref="LineStackingStrategy" /> property.
/// </summary>
public static readonly DependencyProperty LineStackingStrategyProperty =
Block.LineStackingStrategyProperty.AddOwner(typeof(TextBlock));
/// <summary>
/// The LineStackingStrategy property specifies how lines are placed
/// </summary>
public LineStackingStrategy LineStackingStrategy
{
get { return (LineStackingStrategy)GetValue(LineStackingStrategyProperty); }
set { SetValue(LineStackingStrategyProperty, value); }
}
/// <summary>
/// DependencyProperty setter for <see cref="LineStackingStrategy" /> property.
/// </summary>
/// <param name="element">The element to which to write the attached property.</param>
/// <param name="value">The property value to set</param>
public static void SetLineStackingStrategy(DependencyObject element, LineStackingStrategy value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(LineStackingStrategyProperty, value);
}
/// <summary>
/// DependencyProperty getter for <see cref="LineStackingStrategy" /> property.
/// </summary>
/// <param name="element">The element from which to read the attached property.</param>
public static LineStackingStrategy GetLineStackingStrategy(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (LineStackingStrategy)element.GetValue(LineStackingStrategyProperty);
}
/// <summary>
/// DependencyProperty for <see cref="Padding" /> property.
/// </summary>
public static readonly DependencyProperty PaddingProperty =
Block.PaddingProperty.AddOwner(
typeof(TextBlock),
new FrameworkPropertyMetadata(
new Thickness(),
FrameworkPropertyMetadataOptions.AffectsMeasure));
/// <summary>
/// The Padding property specifies the padding of the element.
/// </summary>
public Thickness Padding
{
get { return (Thickness)GetValue(PaddingProperty); }
set { SetValue(PaddingProperty, value); }
}
/// <summary>
/// DependencyProperty for <see cref="TextAlignment" /> property.
/// </summary>
public static readonly DependencyProperty TextAlignmentProperty =
Block.TextAlignmentProperty.AddOwner(typeof(TextBlock));
/// <summary>
/// The TextAlignment property specifies horizontal alignment of the content.
/// </summary>
public TextAlignment TextAlignment
{
get { return (TextAlignment)GetValue(TextAlignmentProperty); }
set { SetValue(TextAlignmentProperty, value); }
}
/// <summary>
/// DependencyProperty setter for <see cref="TextAlignment" /> property.
/// </summary>
/// <param name="element">The element to which to write the attached property.</param>
/// <param name="value">The property value to set</param>
public static void SetTextAlignment(DependencyObject element, TextAlignment value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(TextAlignmentProperty, value);
}
/// <summary>
/// DependencyProperty getter for <see cref="TextAlignment" /> property.
/// </summary>
/// <param name="element">The element from which to read the attached property.</param>
public static TextAlignment GetTextAlignment(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (TextAlignment)element.GetValue(TextAlignmentProperty);
}
/// <summary>
/// DependencyProperty for <see cref="TextTrimming" /> property.
/// </summary>
[CommonDependencyProperty]
public static readonly DependencyProperty TextTrimmingProperty =
DependencyProperty.Register(
"TextTrimming",
typeof(TextTrimming),
typeof(TextBlock),
new FrameworkPropertyMetadata(
TextTrimming.None,
FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender),
new ValidateValueCallback(IsValidTextTrimming));
/// <summary>
/// The TextTrimming property specifies the trimming behavior situation
/// in case of clipping some textual content caused by overflowing the line's box.
/// </summary>
public TextTrimming TextTrimming
{
get { return (TextTrimming)GetValue(TextTrimmingProperty); }
set { SetValue(TextTrimmingProperty, value); }
}
/// <summary>
/// DependencyProperty for <see cref="TextWrapping" /> property.
/// </summary>
[CommonDependencyProperty]
public static readonly DependencyProperty TextWrappingProperty =
DependencyProperty.Register(
"TextWrapping",
typeof(TextWrapping),
typeof(TextBlock),
new FrameworkPropertyMetadata(
TextWrapping.NoWrap,
FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender),
new ValidateValueCallback(IsValidTextWrap));
/// <summary>
/// The TextWrapping property controls whether or not text wraps
/// when it reaches the flow edge of its containing block box.
/// </summary>
public TextWrapping TextWrapping
{
get { return (TextWrapping)GetValue(TextWrappingProperty); }
set { SetValue(TextWrappingProperty, value); }
}
/// <summary>
/// DependencyProperty for hyphenation property.
/// </summary>
public static readonly DependencyProperty IsHyphenationEnabledProperty =
Block.IsHyphenationEnabledProperty.AddOwner(typeof(TextBlock));
/// <summary>
/// CLR property for hyphenation
/// </summary>
public bool IsHyphenationEnabled
{
get { return (bool)GetValue(IsHyphenationEnabledProperty); }
set { SetValue(IsHyphenationEnabledProperty, value); }
}
#endregion Public Dynamic Properties
//-------------------------------------------------------------------
//
// Protected Methods
//
//-------------------------------------------------------------------
#region Protected Methods
/// <summary>
/// Derived classes override this property to enable the Visual code to enumerate
/// the Visual children. Derived classes need to return the number of children
/// from this method.
///
/// By default a Visual does not have any children.
///
/// Remark:
/// During this virtual method the Visual tree must not be modified.
/// </summary>
protected override int VisualChildrenCount
{
get { return _complexContent == null ? 0 : _complexContent.VisualChildren.Count; }
}
/// <summary>
/// Derived class must implement to support Visual children. The method must return
/// the child at the specified index. Index must be between 0 and GetVisualChildrenCount-1.
///
/// By default a Visual does not have any children.
///
/// Remark:
/// During this virtual call it is not valid to modify the Visual tree.
/// </summary>
protected override Visual GetVisualChild(int index)
{
if (_complexContent == null)
{
throw new ArgumentOutOfRangeException("index");
}
return _complexContent.VisualChildren[index];
}
/// <summary>
/// Content measurement.
/// </summary>
/// <param name="constraint">Constraint size.</param>
/// <returns>Computed desired size.</returns>
protected sealed override Size MeasureOverride(Size constraint)
{
VerifyReentrancy();
#if TEXTPANELLAYOUTDEBUG
MS.Internal.PtsHost.TextPanelDebug.BeginScope("TextBlock.MeasureOverride", MS.Internal.PtsHost.TextPanelDebug.Category.MeasureArrange);
MS.Internal.PtsHost.TextPanelDebug.StartTimer("TextBlock.MeasureOverride", MS.Internal.PtsHost.TextPanelDebug.Category.MeasureArrange);
#endif
// Clear and repopulate our text block cache. (Handles multiple measure before arrange)
_textBlockCache = null;
EnsureTextBlockCache();
LineProperties lineProperties = _textBlockCache._lineProperties;
// Hook up our TextContainer event listeners if we haven't yet.
if (CheckFlags(Flags.PendingTextContainerEventInit))
{
Invariant.Assert(_complexContent != null);
InitializeTextContainerListeners();
SetFlags(false, Flags.PendingTextContainerEventInit);
}
// Find out if we can skip measure process. Measure cannot be skipped in following situations:
// a) content is dirty (properties or content)
// b) there are inline objects (they may be dynamically sized)
int lineCount = LineCount;
if ((lineCount > 0) && IsMeasureValid && InlineObjects == null)
{
// Assuming that all of above conditions are true, Measure can be
// skipped in following situations:
// 1) TextTrimming == None and:
// a) Width is the same, or
// b) TextWrapping == NoWrap
// 2) Width is the same and TextWrapping == NoWrap
bool bypassMeasure;
if (lineProperties.TextTrimming == TextTrimming.None)
{
bypassMeasure = DoubleUtil.AreClose(constraint.Width, _referenceSize.Width) || (lineProperties.TextWrapping == TextWrapping.NoWrap);
}
else
{
bypassMeasure =
DoubleUtil.AreClose(constraint.Width, _referenceSize.Width) &&
(lineProperties.TextWrapping == TextWrapping.NoWrap) &&
(DoubleUtil.AreClose(constraint.Height, _referenceSize.Height) || lineCount == 1);
}
if (bypassMeasure)
{
_referenceSize = constraint;
#if TEXTPANELLAYOUTDEBUG
MS.Internal.PtsHost.TextPanelDebug.Log("MeasureOverride bypassed.", MS.Internal.PtsHost.TextPanelDebug.Category.MeasureArrange);
MS.Internal.PtsHost.TextPanelDebug.StopTimer("TextBlock.MeasureOverride", MS.Internal.PtsHost.TextPanelDebug.Category.MeasureArrange);
MS.Internal.PtsHost.TextPanelDebug.EndScope(MS.Internal.PtsHost.TextPanelDebug.Category.MeasureArrange);
#endif
return _previousDesiredSize;
}
}
// Store constraint size, it is used when measuring inline objects.
_referenceSize = constraint;
// Store previous ITextEmbeddable values
bool formattedOnce = CheckFlags(Flags.FormattedOnce);
double baselineOffsetPrevious = _baselineOffset;
// Reset inline objects cache and line metrics cache.
// They will be fully updated during lines formatting.
InlineObjects = null;
// before erasing the line metrics, keep track of how big it was last time
// so that we can initialize the metrics array to that size this time
int subsequentLinesInitialSize = (_subsequentLines == null) ? 1 : _subsequentLines.Count;
ClearLineMetrics();
if (_complexContent != null)
{
_complexContent.TextView.Invalidate();
}
// To determine natural size of the text TextAlignment has to be ignored.
// Since for rendering/hittesting lines are recreated, it can be done without
// any problems.
lineProperties.IgnoreTextAlignment = true;
SetFlags(true, Flags.RequiresAlignment); // Need to update LineMetrics.Start when FinalSize is known.
SetFlags(true, Flags.FormattedOnce);
SetFlags(false, Flags.HasParagraphEllipses);
SetFlags(true, Flags.MeasureInProgress | Flags.TreeInReadOnlyMode);
Size desiredSize = new Size();
bool exceptionThrown = true;
try
{
// Create and format lines until end of paragraph is reached.
// Since we are disposing line object, it can be reused to format following lines.
Line line = CreateLine(lineProperties);
bool endOfParagraph = false;
int dcp = 0;
TextLineBreak textLineBreakIn = null;
Thickness padding = this.Padding;
Size contentSize = new Size(Math.Max(0.0, constraint.Width - (padding.Left + padding.Right)),
Math.Max(0.0, constraint.Height - (padding.Top + padding.Bottom)));
// Make sure that TextFormatter limitations are not exceeded.
//
TextDpi.EnsureValidLineWidth(ref contentSize);
while (!endOfParagraph)
{
using(line)
{
// Format line. Set showParagraphEllipsis flag to false because we do not know whether or not the line will have
// paragraph ellipsis at this time. Since TextBlock is auto-sized we do not know the RenderSize until we finish Measure
line.Format(dcp, contentSize.Width, GetLineProperties(dcp == 0, lineProperties), textLineBreakIn, _textBlockCache._textRunCache, /*Show paragraph ellipsis*/ false);
double lineHeight = CalcLineAdvance(line.Height, lineProperties);
#if DEBUG
LineMetrics metrics = new LineMetrics(contentSize.Width, line.Length, line.Width, lineHeight, line.BaselineOffset, line.HasInlineObjects(), textLineBreakIn);
#else
LineMetrics metrics = new LineMetrics(line.Length, line.Width, lineHeight, line.BaselineOffset, line.HasInlineObjects(), textLineBreakIn);
#endif
if (!CheckFlags(Flags.HasFirstLine))
{
SetFlags(true, Flags.HasFirstLine);
_firstLine = metrics;
}
else
{
if (_subsequentLines == null)
{
_subsequentLines = new List<LineMetrics>(subsequentLinesInitialSize);
}
_subsequentLines.Add(metrics);
}
// Desired width is always max of calculated line widths.
// Desired height is sum of all line heights. But if TextTrimming is on
// do not overflow the requested height with the exception for the first line.
desiredSize.Width = Math.Max(desiredSize.Width, line.GetCollapsedWidth());
if ((lineProperties.TextTrimming == TextTrimming.None) ||
(contentSize.Height >= (desiredSize.Height + lineHeight)) ||
(dcp == 0))
{
// BaselineOffset is always distance from the Text's top
// to the baseline offset of the last line.
_baselineOffset = desiredSize.Height + line.BaselineOffset;
desiredSize.Height += lineHeight;
}
else
{
// Note the fact that there are paragraph ellipses
SetFlags(true, Flags.HasParagraphEllipses);
}
textLineBreakIn = line.GetTextLineBreak();
endOfParagraph = line.EndOfParagraph;
dcp += line.Length;
// don't wrap a line that was artificially broken because of excessive length
if (!endOfParagraph &&
lineProperties.TextWrapping == TextWrapping.NoWrap &&
line.Length == MS.Internal.TextFormatting.TextStore.MaxCharactersPerLine)
{
endOfParagraph = true;
}
}
}
desiredSize.Width += (padding.Left + padding.Right);
desiredSize.Height += (padding.Top + padding.Bottom);
Invariant.Assert(textLineBreakIn == null); // End of paragraph should have no line break record
exceptionThrown = false;
}
finally
{
// Restore original line properties
lineProperties.IgnoreTextAlignment = false;
SetFlags(false, Flags.MeasureInProgress | Flags.TreeInReadOnlyMode);
if(exceptionThrown)
{
_textBlockCache._textRunCache = null;
ClearLineMetrics();
}
}
// Notify ITextHost that ITextEmbeddable values have been changed, if necessary.
if (!DoubleUtil.AreClose(baselineOffsetPrevious, _baselineOffset))
{
CoerceValue(BaselineOffsetProperty);
}
#if TEXTPANELLAYOUTDEBUG
MS.Internal.PtsHost.TextPanelDebug.StopTimer("TextBlock.MeasureOverride", MS.Internal.PtsHost.TextPanelDebug.Category.MeasureArrange);
MS.Internal.PtsHost.TextPanelDebug.EndScope(MS.Internal.PtsHost.TextPanelDebug.Category.MeasureArrange);
#endif
_previousDesiredSize = desiredSize;
return desiredSize;
}
/// <summary>
/// Content arrangement.
/// </summary>
/// <param name="arrangeSize">Size that element should use to arrange itself and its children.</param>
protected sealed override Size ArrangeOverride(Size arrangeSize)
{
VerifyReentrancy();
#if TEXTPANELLAYOUTDEBUG
MS.Internal.PtsHost.TextPanelDebug.BeginScope("TextBlock.ArrangeOverride", MS.Internal.PtsHost.TextPanelDebug.Category.MeasureArrange);
MS.Internal.PtsHost.TextPanelDebug.StartTimer("TextBlock.ArrangeOverride", MS.Internal.PtsHost.TextPanelDebug.Category.MeasureArrange);
#endif
// Remove all existing visuals. If there are inline objects, they will be added below.
if (_complexContent != null)
{
_complexContent.VisualChildren.Clear();
}
ArrayList inlineObjects = InlineObjects;
int lineCount = LineCount;
if (inlineObjects != null && lineCount > 0)
{
bool exceptionThrown = true;
SetFlags(true, Flags.TreeInReadOnlyMode);
SetFlags(true, Flags.ArrangeInProgress);
try
{
EnsureTextBlockCache();
LineProperties lineProperties = _textBlockCache._lineProperties;
double wrappingWidth = CalcWrappingWidth(arrangeSize.Width);
Vector contentOffset = CalcContentOffset(arrangeSize, wrappingWidth);
// Position all inline objects. Recreate only lines that have inline objects
// and call arrange on it. Line.Arrange enumerates all inline objects and
// sets appropriate transform on them.
Line line = CreateLine(lineProperties);
int dcp = 0;
Vector lineOffset = contentOffset;
for (int i = 0; i < lineCount; i++)
{
Debug.Assert(lineCount == LineCount);
LineMetrics lineMetrics = GetLine(i);
if (lineMetrics.HasInlineObjects)
{
using (line)
{
// Check if paragraph ellipsis are added to this line
bool ellipsis = ParagraphEllipsisShownOnLine(i, lineOffset.Y - contentOffset.Y);
line.Format(dcp, wrappingWidth, GetLineProperties(dcp == 0, lineProperties), lineMetrics.TextLineBreak, _textBlockCache._textRunCache, ellipsis);
// Check that lineMetrics length and line length are in sync
//
// We shut off text alignment for measure, ensure we treat same here.
//
// Add inline objects to visual children of the TextBlock visual and
// set appropriate transforms.
line.Arrange(_complexContent.VisualChildren, lineOffset);
}
}
lineOffset.Y += lineMetrics.Height;
dcp += lineMetrics.Length;
}
exceptionThrown = false;
}
finally
{
SetFlags(false, Flags.TreeInReadOnlyMode);
SetFlags(false, Flags.ArrangeInProgress);
if(exceptionThrown)
{
_textBlockCache._textRunCache = null;
ClearLineMetrics();
}
}
}
if (_complexContent != null)
{
Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new DispatcherOperationCallback(OnValidateTextView), EventArgs.Empty);
}
#if TEXTPANELLAYOUTDEBUG
MS.Internal.PtsHost.TextPanelDebug.StopTimer("TextBlock.ArrangeOverride", MS.Internal.PtsHost.TextPanelDebug.Category.MeasureArrange);
MS.Internal.PtsHost.TextPanelDebug.EndScope(MS.Internal.PtsHost.TextPanelDebug.Category.MeasureArrange);
#endif
InvalidateVisual();
return arrangeSize;
}
/// <summary>
/// Render control's content.
/// </summary>
/// <param name="ctx">Drawing context.</param>
protected sealed override void OnRender(DrawingContext ctx)
{
VerifyReentrancy();
if (ctx == null)
{
throw new ArgumentNullException("ctx");
}
// If layout data is not updated do not render the content.
if (!IsLayoutDataValid) { return; }
// Draw background in rectangle.
Brush background = this.Background;
if (background != null)
{
ctx.DrawRectangle(background, null, new Rect(0, 0, RenderSize.Width, RenderSize.Height));
}
SetFlags(false, Flags.RequiresAlignment);
SetFlags(true, Flags.TreeInReadOnlyMode);
try
{
// Line props may be invalid, even if Measure/Arrange is valid - rendering only props are changing.
EnsureTextBlockCache();
LineProperties lineProperties = _textBlockCache._lineProperties;
double wrappingWidth = CalcWrappingWidth(RenderSize.Width);
Vector contentOffset = CalcContentOffset(RenderSize, wrappingWidth);
Point lineOffset = new Point(contentOffset.X, contentOffset.Y);
// NOTE: All inline objects are UIElements and all of them are direct children of
// the TextBlock. Hence visuals for those inline objects are already attached.
// The only responsibility of OnRender is to render text, since transforms for inline
// objects are set during OnArrange.
// Create / format / render all lines.
// Since we are disposing line object, it can be reused to format following lines.
Line line = CreateLine(lineProperties);
int dcp = 0;
bool showParagraphEllipsis = false;
SetFlags(CheckFlags(Flags.HasParagraphEllipses), Flags.RequiresAlignment);
int lineCount = LineCount;
for (int i = 0; i < lineCount; i++)
{
Debug.Assert(lineCount == LineCount);
LineMetrics lineMetrics = GetLine(i);
double contentBottom = Math.Max(0.0, RenderSize.Height - Padding.Bottom);
// Find out if this is the last rendered line
if (CheckFlags(Flags.HasParagraphEllipses))
{
if (i + 1 < lineCount)
{
// Calculate bottom offset for next line
double nextLineBottomOffset = GetLine(i + 1).Height + lineMetrics.Height + lineOffset.Y;
// If the next line will exceed render height by a large margin, we cannot render
// it at all and so we should show ellipsis on this one. However if the next line
// almost fits, we will render it and so there should be no ellipsis
showParagraphEllipsis = DoubleUtil.GreaterThan(nextLineBottomOffset, contentBottom) && !DoubleUtil.AreClose(nextLineBottomOffset, contentBottom);
}
}
// If paragraph ellipsis are enabled, do not render lines that
// extend computed layout size. But if the first line does not fit completely,
// render it anyway.
if (!CheckFlags(Flags.HasParagraphEllipses) ||
(DoubleUtil.LessThanOrClose(lineMetrics.Height + lineOffset.Y, contentBottom) || i == 0))
{
using (line)
{
line.Format(dcp, wrappingWidth, GetLineProperties(dcp == 0, showParagraphEllipsis, lineProperties), lineMetrics.TextLineBreak, _textBlockCache._textRunCache, showParagraphEllipsis);
//
if (!CheckFlags(Flags.HasParagraphEllipses))
{
lineMetrics = UpdateLine(i, lineMetrics, line.Start, line.Width);
}
line.Render(ctx, lineOffset);
lineOffset.Y += lineMetrics.Height;
dcp += lineMetrics.Length;
}
}
}
}
finally
{
SetFlags(false, Flags.TreeInReadOnlyMode);
_textBlockCache = null;
}
}
/// <summary>
/// Notification that a specified property has been invalidated
/// </summary>
/// <param name="e">EventArgs that contains the property, metadata, old value, and new value for this change</param>
protected sealed override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
// Always call base.OnPropertyChanged, otherwise Property Engine will not work.
base.OnPropertyChanged(e);
if (e.IsAValueChange || e.IsASubPropertyChange)
{
if (CheckFlags(Flags.FormattedOnce))
{
FrameworkPropertyMetadata fmetadata = e.Metadata as FrameworkPropertyMetadata;
if (fmetadata != null)
{
bool affectsRender = (fmetadata.AffectsRender &&
(e.IsAValueChange || !fmetadata.SubPropertiesDoNotAffectRender));
if (fmetadata.AffectsMeasure || fmetadata.AffectsArrange || affectsRender)
{
// Will throw an exception, if during measure/arrange/render process.
VerifyTreeIsUnlocked();
// TextRunCache stores properties for every single run fetched so far.
// If there are any property changes, which affect measure, arrange or
// render, invalidate TextRunCache. It will force TextFormatter to refetch
// runs and properties.
// _lineProperties = null;
_textBlockCache = null;
}
}
}
}
}
/// <summary>
/// HitTestCore implements precise hit testing against render contents
/// </summary>
protected sealed override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
{
VerifyReentrancy();
if (hitTestParameters == null)
{
throw new ArgumentNullException("hitTestParameters");
}
Rect r = new Rect(new Point(), RenderSize);
if (r.Contains(hitTestParameters.HitPoint))
{
return new PointHitTestResult(this, hitTestParameters.HitPoint);
}
return null;
}
/// <summary>
/// Hit tests to the correct ContentElement within the ContentHost
/// that the mouse is over.
/// </summary>
/// <param name="point">Mouse coordinates relative to the ContentHost.</param>
protected virtual IInputElement InputHitTestCore(Point point)
{
// If layout data is not updated return 'this'.
if (!IsLayoutDataValid) { return this; }
// Line props may be invalid, even if Measure/Arrange is valid - rendering only props are changing.
LineProperties lineProperties = GetLineProperties();
// If there is only one line and it is already cached, use it to do hit-testing.
// Otherwise, do following:
// a) use cached line information to find which line has been hit,
// b) re-create the line that has been hit,
// c) hit-test the line.
IInputElement ie = null;
double wrappingWidth = CalcWrappingWidth(RenderSize.Width);
Vector contentOffset = CalcContentOffset(RenderSize, wrappingWidth);
point -= contentOffset; // // Take into account content offset.
if (point.X < 0 || point.Y < 0) return this;
ie = null;
int dcp = 0;
double lineOffset = 0;
TextRunCache textRunCache = new TextRunCache();
int lineCount = LineCount;
for (int i = 0; i < lineCount; i++)
{
Debug.Assert(lineCount == LineCount);
LineMetrics lineMetrics = GetLine(i);
if (lineOffset + lineMetrics.Height > point.Y)
{
// The current line has been hit. Format the line and
// retrieve IInputElement from the hit position.
Line line = CreateLine(lineProperties);
using (line)
{
// Check if paragraph ellipsis are rendered
bool ellipsis = ParagraphEllipsisShownOnLine(i, lineOffset);
line.Format(dcp, wrappingWidth, GetLineProperties(dcp == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, ellipsis);
// Verify consistency of line formatting
// Check that lineMetrics.Length is in sync with line.Length
//
//
if ((line.Start <= point.X) && (line.Start + line.Width >= point.X))
{
ie = line.InputHitTest(point.X);
}
}
break; // Line covering the point has been found; no need to continue.
}
dcp += lineMetrics.Length;
lineOffset += lineMetrics.Height;
}
// If nothing has been hit, assume that element itself has been hit.
return (ie != null) ? ie : this;
}
/// <summary>
/// Returns an ICollection of bounding rectangles for the given ContentElement
/// </summary>
/// <param name="child">
/// Content element for which rectangles are required
/// </param>
/// <remarks>
/// Looks at the ContentElement e line by line and gets rectangle bounds for each line
/// </remarks>
protected virtual ReadOnlyCollection<Rect> GetRectanglesCore(ContentElement child)
{
if (child == null)
{
throw new ArgumentNullException("child");
}
// If layout data is not updated we assume that we will not be able to find the element we need and throw excception
if (!IsLayoutDataValid)
{
// return empty collection
return new ReadOnlyCollection<Rect>(new List<Rect>(0));
}
// Line props may be invalid, even if Measure/Arrange is valid - rendering only props are changing.
LineProperties lineProperties = GetLineProperties();
// Check for complex content
if (_complexContent == null || !(_complexContent.TextContainer is TextContainer))
{
// return empty collection
return new ReadOnlyCollection<Rect>(new List<Rect>(0));
}
// First find the element start and end position
TextPointer start = FindElementPosition((IInputElement)child);
if (start == null)
{
return new ReadOnlyCollection<Rect>(new List<Rect>(0));
}
TextPointer end = null;
if (child is TextElement)
{
end = new TextPointer(((TextElement)child).ElementEnd);
}
else if (child is FrameworkContentElement)
{
end = new TextPointer(start);
end.MoveByOffset(+1);
}
if (end == null)
{
return new ReadOnlyCollection<Rect>(new List<Rect>(0));
}
int startOffset = _complexContent.TextContainer.Start.GetOffsetToPosition(start);
int endOffset = _complexContent.TextContainer.Start.GetOffsetToPosition(end);
int lineIndex = 0;
int lineOffset = 0;
double lineHeightOffset = 0;
int lineCount = LineCount;
while (startOffset >= (lineOffset + GetLine(lineIndex).Length) && lineIndex < lineCount)
{
Debug.Assert(lineCount == LineCount);
lineOffset += GetLine(lineIndex).Length;
lineIndex++;
lineHeightOffset += GetLine(lineIndex).Height;
}
Debug.Assert(lineIndex < lineCount);
int lineStart = lineOffset;
List<Rect> rectangles = new List<Rect>();
double wrappingWidth = CalcWrappingWidth(RenderSize.Width);
TextRunCache textRunCache = new TextRunCache();
Vector contentOffset = CalcContentOffset(RenderSize, wrappingWidth);
do
{
Debug.Assert(lineCount == LineCount);
// Check that line index never exceeds line count
Debug.Assert(lineIndex < lineCount);
// Create lines as long as they are spanned by the element
LineMetrics lineMetrics = GetLine(lineIndex);
Line line = CreateLine(lineProperties);
using (line)
{
// Check if paragraph ellipsis are rendered
bool ellipsis = ParagraphEllipsisShownOnLine(lineIndex, lineOffset);
line.Format(lineStart, wrappingWidth, GetLineProperties(lineIndex == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, ellipsis);
// Verify consistency of line formatting
//
if (lineMetrics.Length == line.Length)
{
//MS.Internal.Invariant.Assert(lineMetrics.Length == line.Length, "Line length is out of sync");
//Debug.Assert(DoubleUtil.AreClose(CalcLineAdvance(line.Height, lineProperties), lineMetrics.Height), "Line height is out of sync.");
int boundStart = (startOffset >= lineStart) ? startOffset : lineStart;
int boundEnd = (endOffset < lineStart + lineMetrics.Length) ? endOffset : lineStart + lineMetrics.Length;
double xOffset = contentOffset.X;
double yOffset = contentOffset.Y + lineHeightOffset;
List<Rect> lineBounds = line.GetRangeBounds(boundStart, boundEnd - boundStart, xOffset, yOffset);
Debug.Assert(lineBounds.Count > 0);
rectangles.AddRange(lineBounds);
}
}
lineStart += lineMetrics.Length;
lineHeightOffset += lineMetrics.Height;
lineIndex++;
}
while (endOffset > lineStart);
// Rectangles collection must be non-null
Invariant.Assert(rectangles != null);
return new ReadOnlyCollection<Rect>(rectangles);
}
/// <summary>
/// Returns elements hosted by the content host as an enumerator class
/// </summary>
protected virtual IEnumerator<IInputElement> HostedElementsCore
{
get
{
if(CheckFlags(Flags.ContentChangeInProgress))
{
#pragma warning suppress 6503 // IEnumerator.Current is documented to throw this exception
throw new InvalidOperationException(SR.Get(SRID.TextContainerChangingReentrancyInvalid));
}
if (_complexContent == null || !(_complexContent.TextContainer is TextContainer))
{
// Return empty collection
return new HostedElements(new ReadOnlyCollection<TextSegment>(new List<TextSegment>(0)));
}
// Create a TextSegment from TextContainer, use it to return enumerator
System.Collections.Generic.List<TextSegment> textSegmentsList = new System.Collections.Generic.List<TextSegment>(1);
TextSegment textSegment = new TextSegment(_complexContent.TextContainer.Start, _complexContent.TextContainer.End);
textSegmentsList.Insert(0, textSegment);
ReadOnlyCollection<TextSegment> textSegments = new ReadOnlyCollection<TextSegment>(textSegmentsList);
// Return enumerator created from textSegments
return new HostedElements(textSegments);
}
}
/// <summary>
/// Called when a UIElement-derived class which is hosted by a IContentHost changes its DesiredSize
/// </summary>
/// <param name="child">
/// Child element whose DesiredSize has changed
/// </param>
protected virtual void OnChildDesiredSizeChangedCore(UIElement child)
{
this.OnChildDesiredSizeChanged(child);
}
/// <summary>
/// Creates AutomationPeer (<see cref="UIElement.OnCreateAutomationPeer"/>)
/// </summary>
protected override AutomationPeer OnCreateAutomationPeer()
{
return new TextBlockAutomationPeer(this);
}
#endregion Protected Methods
//-------------------------------------------------------------------
//
// Internal Methods
//
//-------------------------------------------------------------------
#region Internal Methods
/// <summary>
/// remove a child from TextBlock's collection (only Visually)
/// Used by ComplexLine.cs, etc
/// </summary>
internal void RemoveChild(Visual child)
{
if (_complexContent != null)
{
_complexContent.VisualChildren.Remove(child);
}
}
/// <summary>
/// Sets external text container as a content for this TextBlock element.
/// </summary>
/// <param name="textContainer"></param>
/// <remarks>
/// External text container will remain owned by its current owner.
/// This TextBlock element is not supposed to own it,
/// which means that this TextBlock will have empty Children collection,
/// and all top level elements in passed TextContainer will
/// remain parented in their respective owner.
/// </remarks>
internal void SetTextContainer(ITextContainer textContainer)
{
// Detach from a previous container
if (_complexContent != null)
{
_complexContent.Detach(this);
_complexContent = null;
SetFlags(false, Flags.PendingTextContainerEventInit);
}
// Attach new container
if (textContainer != null)
{
_complexContent = null;
EnsureComplexContent(textContainer);
}
SetFlags(false, Flags.ContentChangeInProgress);
// Invalidate measure in responce to invalidated content.
InvalidateMeasure();
InvalidateVisual(); //invlaidate rendering in addition to sizing info
}
//-------------------------------------------------------------------
// Measure child UIElement.
//
// inlineObject - hosted inline object to measure.
//
// Returns: Size of the inline object.
//-------------------------------------------------------------------
internal Size MeasureChild(InlineObject inlineObject)
{
Debug.Assert(_complexContent != null, "Inline objects are supported only in complex content.");
Size desiredSize;
// Measure child only during measure pass. If not during measuring
// use RenderSize.
if (CheckFlags(Flags.MeasureInProgress))
{
// Measure inline objects. Original size constraint is passed,
// because inline object size should not be dependent on position
// inside a text line. It should not be also bigger than Text itself.
Thickness padding = this.Padding;
Size contentSize = new Size(Math.Max(0.0, _referenceSize.Width - (padding.Left + padding.Right)),
Math.Max(0.0, _referenceSize.Height - (padding.Top + padding.Bottom)));
inlineObject.Element.Measure(contentSize);
desiredSize = inlineObject.Element.DesiredSize;
// Store inline object in the cache.
ArrayList inlineObjects = InlineObjects;
bool alreadyCached = false;
if (inlineObjects == null)
{
InlineObjects = inlineObjects = new ArrayList(1);
}
else
{
// Find out if inline object is already cached.
for (int index = 0; index < inlineObjects.Count; index++)
{
if (((InlineObject)inlineObjects[index]).Dcp == inlineObject.Dcp)
{
Debug.Assert(((InlineObject)inlineObjects[index]).Element == inlineObject.Element, "InlineObject cache is out of sync.");
alreadyCached = true;
break;
}
}
}
if (!alreadyCached)
{
inlineObjects.Add(inlineObject);
}
}
else
{
desiredSize = inlineObject.Element.DesiredSize;
}
return desiredSize;
}
/// <summary>
/// Gives a string representation of this object.
/// </summary>
internal override string GetPlainText()
{
if (_complexContent != null)
{
return TextRangeBase.GetTextInternal(_complexContent.TextContainer.Start, _complexContent.TextContainer.End);
}
else
{
if (_contentCache != null)
{
return _contentCache;
}
}
return String.Empty;
}
/// <summary>
/// Returns a new array of LineResults for the paragraph's lines.
/// </summary>
internal ReadOnlyCollection<LineResult> GetLineResults()
{
#if TEXTPANELLAYOUTDEBUG
MS.Internal.PtsHost.TextPanelDebug.IncrementCounter("TextBlock.GetLines", MS.Internal.PtsHost.TextPanelDebug.Category.TextView);
#endif
Invariant.Assert(IsLayoutDataValid);
// Proper line alignment has to be done bofore LineResults are created.
// Owherwise Line.Start may have wrong value.
if (CheckFlags(Flags.RequiresAlignment))
{
AlignContent();
}
// Calculate content offset.
double wrappingWidth = CalcWrappingWidth(RenderSize.Width);
Vector contentOffset = CalcContentOffset(RenderSize, wrappingWidth);
// Create line results
int lineCount = LineCount;
List<LineResult> lines = new List<LineResult>(lineCount);
int dcp = 0;
double lineOffset = 0;
for (int lineIndex = 0; lineIndex < lineCount; lineIndex++)
{
Debug.Assert(lineCount == LineCount);
LineMetrics lineMetrics = GetLine(lineIndex);
Rect layoutBox = new Rect(contentOffset.X + lineMetrics.Start, contentOffset.Y + lineOffset, lineMetrics.Width, lineMetrics.Height);
lines.Add(new TextLineResult(this, dcp, lineMetrics.Length, layoutBox, lineMetrics.Baseline, lineIndex));
lineOffset += lineMetrics.Height;
dcp += lineMetrics.Length;
}
return new ReadOnlyCollection<LineResult>(lines);
}
/// <summary>
/// Retrieves detailed information about a line of text.
/// </summary>
/// <param name="dcp">Index of the first character in the line.</param>
/// <param name="index"> Index of the line</param>
/// <param name="lineVOffset"> Vertical offset of the line</param>
/// <param name="cchContent">Number of content characters in the line.</param>
/// <param name="cchEllipses">Number of content characters hidden by ellipses.</param>
internal void GetLineDetails(int dcp, int index, double lineVOffset, out int cchContent, out int cchEllipses)
{
Invariant.Assert(IsLayoutDataValid);
Invariant.Assert(index >= 0 && index < LineCount);
// Line props may be invalid, even if Measure/Arrange is valid - rendering only props are changing.
LineProperties lineProperties = GetLineProperties();
double wrappingWidth = CalcWrappingWidth(RenderSize.Width);
TextRunCache textRunCache = new TextRunCache();
// Retrieve details from the line.
using(Line line = CreateLine(lineProperties))
{
// Format line. Set showParagraphEllipsis flag to false
TextLineBreak textLineBreak = GetLine(index).TextLineBreak;
bool ellipsis = ParagraphEllipsisShownOnLine(index, lineVOffset);
line.Format(dcp, wrappingWidth, GetLineProperties(dcp == 0, lineProperties), textLineBreak, textRunCache, ellipsis);
MS.Internal.Invariant.Assert(GetLine(index).Length == line.Length, "Line length is out of sync");
cchContent = line.ContentLength;
cchEllipses = line.GetEllipsesLength();
}
}
/// <summary>
/// Retrieve text position from the distance (relative to the beginning
/// of specified line).
/// </summary>
/// <param name="dcp">Index of the first character in the line.</param>
/// <param name="distance">Distance relative to the beginning of the line.</param>
/// <param name="lineVOffset">
/// Vertical offset of the line in which the position lies,
/// </param>
/// <param name="index">
/// Index of the line
/// </param>
/// <returns>
/// A text position and its orientation matching or closest to the distance.
/// </returns>
internal ITextPointer GetTextPositionFromDistance(int dcp, double distance, double lineVOffset, int index)
{
#if TEXTPANELLAYOUTDEBUG
MS.Internal.PtsHost.TextPanelDebug.StartTimer("TextBlock.GetTextPositionFromDistance", MS.Internal.PtsHost.TextPanelDebug.Category.TextView);
#endif
Invariant.Assert(IsLayoutDataValid);
// Line props may be invalid, even if Measure/Arrange is valid - rendering only props are changing.
LineProperties lineProperties = GetLineProperties();
EnsureComplexContent(); // TextOM access requires complex content.
double wrappingWidth = CalcWrappingWidth(RenderSize.Width);
Vector contentOffset = CalcContentOffset(RenderSize, wrappingWidth);
distance -= contentOffset.X;
lineVOffset -= contentOffset.Y;
TextRunCache textRunCache = new TextRunCache();
ITextPointer pos;
using(Line line = CreateLine(lineProperties))
{
MS.Internal.Invariant.Assert(index >= 0 && index < LineCount);
TextLineBreak textLineBreak = GetLine(index).TextLineBreak;
bool ellipsis = ParagraphEllipsisShownOnLine(index, lineVOffset);
line.Format(dcp, wrappingWidth, GetLineProperties(dcp == 0, lineProperties), textLineBreak, textRunCache, ellipsis);
MS.Internal.Invariant.Assert(GetLine(index).Length == line.Length, "Line length is out of sync");
CharacterHit charIndex = line.GetTextPositionFromDistance(distance);
LogicalDirection logicalDirection;
logicalDirection = (charIndex.TrailingLength > 0) ? LogicalDirection.Backward : LogicalDirection.Forward;
pos = _complexContent.TextContainer.Start.CreatePointer(charIndex.FirstCharacterIndex + charIndex.TrailingLength, logicalDirection);
}
#if TEXTPANELLAYOUTDEBUG
MS.Internal.PtsHost.TextPanelDebug.StopTimer("TextBlock.GetTextPositionFromDistance", MS.Internal.PtsHost.TextPanelDebug.Category.TextView);
#endif
return pos;
}
/// <summary>
/// Retrieves bounds of an object/character at the specified TextPointer.
/// Throws IndexOutOfRangeException if position is out of range.
/// </summary>
/// <param name="orientedPosition">Position of an object/character.</param>
/// <returns>Bounds of an object/character.</returns>
internal Rect GetRectangleFromTextPosition(ITextPointer orientedPosition)
{
#if TEXTPANELLAYOUTDEBUG
MS.Internal.PtsHost.TextPanelDebug.StartTimer("TextBlock.GetRectangleFromTextPosition", MS.Internal.PtsHost.TextPanelDebug.Category.TextView);
#endif
Invariant.Assert(IsLayoutDataValid);
Invariant.Assert(orientedPosition != null);
// Line props may be invalid, even if Measure/Arrange is valid - rendering only props are changing.
LineProperties lineProperties = GetLineProperties();
EnsureComplexContent();
// From TextFormatter get rectangle of a single character.
// If orientation is Backward, get the length of th previous character.
int characterIndex = _complexContent.TextContainer.Start.GetOffsetToPosition(orientedPosition);
int originalCharacterIndex = characterIndex;
if (orientedPosition.LogicalDirection == LogicalDirection.Backward && characterIndex > 0)
{
--characterIndex;
}
double wrappingWidth = CalcWrappingWidth(RenderSize.Width);
Vector contentOffset = CalcContentOffset(RenderSize, wrappingWidth);
double lineOffset = 0;
int dcp = 0;
TextRunCache textRunCache = new TextRunCache();
Rect rect = Rect.Empty;
FlowDirection flowDirection = FlowDirection.LeftToRight;
int lineCount = LineCount;
for (int i = 0; i < lineCount; i++)
{
Debug.Assert(lineCount == LineCount);
LineMetrics lineMetrics = GetLine(i);
// characterIndex needs to be within line range. If position points to
// dcp + line.Length, it means that the next line starts from such position,
// hence go to the next line.
// But if this is the last line (EOP character), get rectangle form the last
// character of the line.
if (dcp + lineMetrics.Length > characterIndex ||
((dcp + lineMetrics.Length == characterIndex) && (i == lineCount - 1)))
{
using(Line line = CreateLine(lineProperties))
{
bool ellipsis = ParagraphEllipsisShownOnLine(i, lineOffset);
line.Format(dcp, wrappingWidth, GetLineProperties(dcp == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, ellipsis);
// Check consistency of line length
MS.Internal.Invariant.Assert(lineMetrics.Length == line.Length, "Line length is out of sync");
rect = line.GetBoundsFromTextPosition(characterIndex, out flowDirection);
}
break;
}
dcp += lineMetrics.Length;
lineOffset += lineMetrics.Height;
}
if (!rect.IsEmpty) // Empty rects can't be modified
{
rect.X += contentOffset.X;
rect.Y += contentOffset.Y + lineOffset;
// Return only TopLeft and Height.
// Adjust rect.Left by taking into account flow direction of the
// content and orientation of input position.
if (lineProperties.FlowDirection != flowDirection)
{
if (orientedPosition.LogicalDirection == LogicalDirection.Forward || originalCharacterIndex == 0)
{
rect.X = rect.Right;
}
}
else
{
// NOTE: check for 'originalCharacterIndex > 0' is only required for position at the beginning
// content with Backward orientation. This should not be a valid position.
// Remove it later
if (orientedPosition.LogicalDirection == LogicalDirection.Backward && originalCharacterIndex > 0)
{
rect.X = rect.Right;
}
}
rect.Width = 0;
}
#if TEXTPANELLAYOUTDEBUG
MS.Internal.PtsHost.TextPanelDebug.StopTimer("TextBlock.GetRectangleFromTextPosition", MS.Internal.PtsHost.TextPanelDebug.Category.TextView);
#endif
return rect;
}
/// <summary>
/// Implementation of TextParagraphView.GetTightBoundingGeometryFromTextPositions.
/// <seealso cref="TextParagraphView.GetTightBoundingGeometryFromTextPositions"/>
/// </summary>
internal Geometry GetTightBoundingGeometryFromTextPositions(ITextPointer startPosition, ITextPointer endPosition)
{
#if TEXTPANELLAYOUTDEBUG
MS.Internal.PtsHost.TextPanelDebug.StartTimer("TextBlock.GetTightBoundingGeometryFromTextPositions", MS.Internal.PtsHost.TextPanelDebug.Category.TextView);
#endif
Invariant.Assert(IsLayoutDataValid);
Invariant.Assert(startPosition != null);
Invariant.Assert(endPosition != null);
Invariant.Assert(startPosition.CompareTo(endPosition) <= 0);
Geometry geometry = null;
// Line props may be invalid, even if Measure/Arrange is valid - rendering only props are changing.
LineProperties lineProperties = GetLineProperties();
EnsureComplexContent(); // TextOM access requires complex content.
int dcpPositionStart = _complexContent.TextContainer.Start.GetOffsetToPosition(startPosition);
int dcpPositionEnd = _complexContent.TextContainer.Start.GetOffsetToPosition(endPosition);
double wrappingWidth = CalcWrappingWidth(RenderSize.Width);
Vector contentOffset = CalcContentOffset(RenderSize, wrappingWidth);
TextRunCache textRunCache = new TextRunCache();
Line line = CreateLine(lineProperties);
int dcpLineStart = 0;
ITextPointer endOfLineTextPointer = _complexContent.TextContainer.Start.CreatePointer(0);
double lineOffset = 0;
int lineCount = LineCount;
for (int i = 0, count = lineCount; i < count; ++i)
{
LineMetrics lineMetrics = GetLine(i);
if (dcpPositionEnd <= dcpLineStart)
{
// this line starts after the range's end.
// safe to break from the loop.
break;
}
int dcpLineEnd = dcpLineStart + lineMetrics.Length;
endOfLineTextPointer.MoveByOffset(lineMetrics.Length);
if (dcpPositionStart < dcpLineEnd)
{
using (line)
{
bool ellipsis = ParagraphEllipsisShownOnLine(i, lineOffset);
line.Format(dcpLineStart, wrappingWidth, GetLineProperties(dcpLineStart == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, ellipsis);
if (Invariant.Strict)
{
// Check consistency of line formatting
MS.Internal.Invariant.Assert(GetLine(i).Length == line.Length, "Line length is out of sync");
}
int dcpStart = Math.Max(dcpLineStart, dcpPositionStart);
int dcpEnd = Math.Min(dcpLineEnd, dcpPositionEnd);
if (dcpStart != dcpEnd)
{
IList<Rect> aryTextBounds = line.GetRangeBounds(dcpStart, dcpEnd - dcpStart, contentOffset.X, contentOffset.Y + lineOffset);
if (aryTextBounds.Count > 0)
{
int j = 0;
int c = aryTextBounds.Count;
do
{
Rect rect = aryTextBounds[j];
if (j == (c - 1)
&& dcpPositionEnd >= dcpLineEnd
&& TextPointerBase.IsNextToAnyBreak(endOfLineTextPointer, LogicalDirection.Backward))
{
double endOfParaGlyphWidth = FontSize * CaretElement.c_endOfParaMagicMultiplier;
rect.Width = rect.Width + endOfParaGlyphWidth;
}
RectangleGeometry rectGeometry = new RectangleGeometry(rect);
CaretElement.AddGeometry(ref geometry, rectGeometry);
} while (++j < c);
}
}
}
}
dcpLineStart += lineMetrics.Length;
lineOffset += lineMetrics.Height;
}
#if TEXTPANELLAYOUTDEBUG
MS.Internal.PtsHost.TextPanelDebug.StopTimer("TextBlock.GetTightBoundingGeometryFromTextPositions", MS.Internal.PtsHost.TextPanelDebug.Category.TextView);
#endif
return (geometry);
}
/// <summary>
/// Determines if the given position is at the edge of a caret unit
/// in the specified direction, and returns true if it is and false otherwise.
/// Used by the ITextView.IsCaretAtUnitBoundary(ITextPointer position) in
/// TextParagraphView
/// </summary>
/// <param name="position">
/// Position to test.
/// </param>
/// <param name="dcp">
/// Offset of the current position from start of TextContainer
/// </param>
/// <param name="lineIndex">
/// Index of line in which position is found
/// </param>
internal bool IsAtCaretUnitBoundary(ITextPointer position, int dcp, int lineIndex)
{
Invariant.Assert(IsLayoutDataValid);
// Line props may be invalid, even if Measure/Arrange is valid - rendering only props are changing.
LineProperties lineProperties = GetLineProperties();
EnsureComplexContent();
TextRunCache textRunCache = new TextRunCache();
bool isAtCaretUnitBoundary = false;
int characterIndex = _complexContent.TextContainer.Start.GetOffsetToPosition(position);
CharacterHit charHit = new CharacterHit();
if (position.LogicalDirection == LogicalDirection.Backward)
{
if (characterIndex > dcp)
{
// Go to trailing edge of previous character
charHit = new CharacterHit(characterIndex - 1, 1);
}
else
{
// We should not be at line's start dcp with backward context, except in case this is the first line. This is not
// a unit boundary
return false;
}
}
else if (position.LogicalDirection == LogicalDirection.Forward)
{
// Get leading edge of this character index
charHit = new CharacterHit(characterIndex, 0);
}
LineMetrics lineMetrics = GetLine(lineIndex);
double wrappingWidth = CalcWrappingWidth(RenderSize.Width);
using(Line line = CreateLine(lineProperties))
{
// Format line. Set showParagraphEllipsis flag to false since we are not using information about
// ellipsis to change line offsets in this case.
line.Format(dcp, wrappingWidth, GetLineProperties(lineIndex == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, false);
// Check consistency of line formatting
MS.Internal.Invariant.Assert(lineMetrics.Length == line.Length, "Line length is out of sync");
isAtCaretUnitBoundary = line.IsAtCaretCharacterHit(charHit);
}
return isAtCaretUnitBoundary;
}
/// <summary>
/// Finds and returns the next position at the edge of a caret unit in
/// specified direction.
/// </summary>
/// <param name="position">
/// Initial text position of an object/character.
/// </param>
/// <param name="direction">
/// If Forward, this method returns the "caret unit" position following
/// the initial position.
/// If Backward, this method returns the caret unit" position preceding
/// the initial position.
/// </param>
/// <param name="dcp">
/// Offset of the current position from start of TextContainer
/// </param>
/// <param name="lineIndex">
/// Index of line in which position is found
/// </param>
internal ITextPointer GetNextCaretUnitPosition(ITextPointer position, LogicalDirection direction, int dcp, int lineIndex)
{
Invariant.Assert(IsLayoutDataValid);
// Line props may be invalid, even if Measure/Arrange is valid - rendering only props are changing.
LineProperties lineProperties = GetLineProperties();
EnsureComplexContent();
int characterIndex = _complexContent.TextContainer.Start.GetOffsetToPosition(position);
// Process special cases
if (characterIndex == dcp && direction == LogicalDirection.Backward)
{
// Start of line
if (lineIndex == 0)
{
// First line. Cannot go back any further
return position;
}
else
{
// Change lineIndex and dcp
Debug.Assert(lineIndex > 0);
--lineIndex;
dcp -= GetLine(lineIndex).Length;
Debug.Assert(dcp >= 0);
}
}
else if (characterIndex == (dcp + GetLine(lineIndex).Length) && direction == LogicalDirection.Forward)
{
// End of line
int lineCount = LineCount;
if (lineIndex == lineCount - 1)
{
// Cannot go down any further
return position;
}
else
{
// Change lineIndex and dcp to next line
Debug.Assert(lineIndex < lineCount - 1);
dcp += GetLine(lineIndex).Length;
++lineIndex;
}
}
TextRunCache textRunCache = new TextRunCache();
// Creat CharacterHit from characterIndex and call line APIs
double wrappingWidth = CalcWrappingWidth(RenderSize.Width);
CharacterHit textSourceCharacterIndex = new CharacterHit(characterIndex, 0);
CharacterHit nextCharacterHit;
LineMetrics lineMetrics = GetLine(lineIndex);
using(Line line = CreateLine(lineProperties))
{
// Format line. Set showParagraphEllipsis flag to false since we are not using information about
// ellipsis to change line offsets in this case.
line.Format(dcp, wrappingWidth, GetLineProperties(lineIndex == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, false);
// Check consistency of line formatting
MS.Internal.Invariant.Assert(lineMetrics.Length == line.Length, "Line length is out of sync");
if (direction == LogicalDirection.Forward)
{
// Get the next caret position from the line
nextCharacterHit = line.GetNextCaretCharacterHit(textSourceCharacterIndex);
}
else
{
// Get previous caret position from the line
nextCharacterHit = line.GetPreviousCaretCharacterHit(textSourceCharacterIndex);
}
}
// Determine logical direction for next caret index and create TextPointer from it
LogicalDirection logicalDirection;
if ((nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength == (dcp + GetLine(lineIndex).Length)) && direction == LogicalDirection.Forward)
{
// Going forward brought us to the end of a line, context must be forward for next line
if (lineIndex == LineCount - 1)
{
// last line so context must stay backward
logicalDirection = LogicalDirection.Backward;
}
else
{
logicalDirection = LogicalDirection.Forward;
}
}
else if ((nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength == dcp) && direction == LogicalDirection.Backward)
{
// Going forward brought us to the start of a line, context must be backward for previous line
if (dcp == 0)
{
// First line, so we will stay forward
logicalDirection = LogicalDirection.Forward;
}
else
{
logicalDirection = LogicalDirection.Backward;
}
}
else
{
logicalDirection = (nextCharacterHit.TrailingLength > 0) ? LogicalDirection.Backward : LogicalDirection.Forward;
}
ITextPointer nextCaretPosition = _complexContent.TextContainer.Start.CreatePointer(nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength, logicalDirection);
// Return nextCaretPosition
return nextCaretPosition;
}
/// <summary>
/// Finds and returns the position after backspace at the edge of a caret unit in
/// specified direction.
/// </summary>
/// <param name="position">
/// Initial text position of an object/character.
/// </param>
/// <param name="dcp">
/// Offset of the current position from start of TextContainer
/// </param>
/// <param name="lineIndex">
/// Index of line in which position is found
/// </param>
internal ITextPointer GetBackspaceCaretUnitPosition(ITextPointer position, int dcp, int lineIndex)
{
Invariant.Assert(IsLayoutDataValid);
// Line props may be invalid, even if Measure/Arrange is valid - rendering only props are changing.
LineProperties lineProperties = GetLineProperties();
EnsureComplexContent();
// Get character index for position
int characterIndex = _complexContent.TextContainer.Start.GetOffsetToPosition(position);
// Process special cases
if (characterIndex == dcp)
{
if (lineIndex == 0)
{
// Cannot go back any further
return position;
}
else
{
// Change lineIndex and dcp to previous line
Debug.Assert(lineIndex > 0);
--lineIndex;
dcp -= GetLine(lineIndex).Length;
Debug.Assert(dcp >= 0);
}
}
double wrappingWidth = CalcWrappingWidth(RenderSize.Width);
CharacterHit textSourceCharacterIndex = new CharacterHit(characterIndex, 0);
CharacterHit backspaceCharacterHit;
LineMetrics lineMetrics = GetLine(lineIndex);
TextRunCache textRunCache = new TextRunCache();
// Create and Format line
using(Line line = CreateLine(lineProperties))
{
// Format line. Set showParagraphEllipsis flag to false since we are not using information about
// ellipsis to change line offsets in this case.
line.Format(dcp, wrappingWidth, GetLineProperties(lineIndex == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, false);
// Check consistency of line formatting
MS.Internal.Invariant.Assert(lineMetrics.Length == line.Length, "Line length is out of sync");
backspaceCharacterHit = line.GetBackspaceCaretCharacterHit(textSourceCharacterIndex);
}
// Get CharacterHit and call line API
// Determine logical direction for next caret index and create TextPointer from it
LogicalDirection logicalDirection;
if (backspaceCharacterHit.FirstCharacterIndex + backspaceCharacterHit.TrailingLength == dcp)
{
// Going forward brought us to the start of a line, context must be backward for previous line
if (dcp == 0)
{
// First line, so we will stay forward
logicalDirection = LogicalDirection.Forward;
}
else
{
logicalDirection = LogicalDirection.Backward;
}
}
else
{
logicalDirection = (backspaceCharacterHit.TrailingLength > 0) ? LogicalDirection.Backward : LogicalDirection.Forward;
}
ITextPointer backspaceCaretPosition = _complexContent.TextContainer.Start.CreatePointer(backspaceCharacterHit.FirstCharacterIndex + backspaceCharacterHit.TrailingLength, logicalDirection);
// Return backspaceCaretPosition
return backspaceCaretPosition;
}
#endregion Internal methods
//-------------------------------------------------------------------
//
// Internal Properties
//
//-------------------------------------------------------------------
#region Internal Properties
//-------------------------------------------------------------------
// Text formatter object
//-------------------------------------------------------------------
internal TextFormatter TextFormatter
{
get
{
TextFormattingMode textFormattingMode = TextOptions.GetTextFormattingMode(this);
if (TextFormattingMode.Display == textFormattingMode)
{
if (_textFormatterDisplay == null)
{
_textFormatterDisplay = System.Windows.Media.TextFormatting.TextFormatter.FromCurrentDispatcher(textFormattingMode);
}
return _textFormatterDisplay;
}
else
{
if (_textFormatterIdeal == null)
{
_textFormatterIdeal = System.Windows.Media.TextFormatting.TextFormatter.FromCurrentDispatcher(textFormattingMode);
}
return _textFormatterIdeal;
}
}
}
//-------------------------------------------------------------------
// Text container.
//-------------------------------------------------------------------
internal ITextContainer TextContainer
{
get
{
EnsureComplexContent();
return _complexContent.TextContainer;
}
}
//-------------------------------------------------------------------
// TextView
//-------------------------------------------------------------------
internal ITextView TextView
{
get
{
EnsureComplexContent();
return _complexContent.TextView;
}
}
//-------------------------------------------------------------------
// Highlights
//-------------------------------------------------------------------
internal Highlights Highlights
{
get
{
EnsureComplexContent();
return _complexContent.Highlights;
}
}
//-------------------------------------------------------------------
// TextBlock paragraph properties.
//-------------------------------------------------------------------
internal LineProperties ParagraphProperties
{
get
{
LineProperties lineProperties = GetLineProperties();
return lineProperties;
}
}
//-------------------------------------------------------------------
// IsLayoutDataValid
//-------------------------------------------------------------------
internal bool IsLayoutDataValid
{
get
{
return IsMeasureValid && IsArrangeValid && // Measure and Arrange are valid
CheckFlags(Flags.HasFirstLine) &&
!CheckFlags(Flags.ContentChangeInProgress) &&
!CheckFlags(Flags.MeasureInProgress) &&
!CheckFlags(Flags.ArrangeInProgress); // Content is not currently changeing
}
}
//-------------------------------------------------------------------
// HasComplexContent
//-------------------------------------------------------------------
internal bool HasComplexContent
{
get
{
return (_complexContent != null);
}
}
//-------------------------------------------------------------------
// IsTypographyDefaultValue
//-------------------------------------------------------------------
internal bool IsTypographyDefaultValue
{
get
{
return !CheckFlags(Flags.IsTypographySet);
}
}
//-------------------------------------------------------------------
// InlineObjects
//-------------------------------------------------------------------
private ArrayList InlineObjects
{
get { return (_complexContent == null) ? null : _complexContent.InlineObjects; }
set { if (_complexContent != null) _complexContent.InlineObjects = value; }
}
//-------------------------------------------------------------------
// Is this TextBlock control being used by a ContentPresenter/ HyperLink
// to host its content. If it is then TextBlock musn't try to disconnect the
// logical parent pointer for the content. This flag allows the TextBlock
// to discover this special scenario and behave differently.
//-------------------------------------------------------------------
internal bool IsContentPresenterContainer
{
get { return CheckFlags(Flags.IsContentPresenterContainer); }
set { SetFlags(value, Flags.IsContentPresenterContainer); }
}
// typography properties changed, no cache for this, just reset the flag
private static void OnTypographyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((TextBlock) d).SetFlags(true, Flags.IsTypographySet);
}
#endregion Internal Properties
//-------------------------------------------------------------------
//
// Private Methods
//
//-------------------------------------------------------------------
#region Private Methods
/// <summary>
/// Raise TextView.Updated event, if TextView is in a valid state.
/// </summary>
private object OnValidateTextView(object arg)
{
if (IsLayoutDataValid && _complexContent != null)
{
_complexContent.TextView.OnUpdated();
}
return null;
}
// Inserts text run into TextBlock in a form consistent with flow schema requirements.
//
// TextBlock has dual role as a text container -
// plain text for TextBox and rich inline-collection content for everything else.
// In case of plain text we insert only string into the text container,
// in all other cases we must wrap eact rext run by Run inline element.
private static void InsertTextRun(ITextPointer position, string text, bool whitespacesIgnorable)
{
// We distinguish these two cases by parent of TextContainer:
// for plain text case it is TextBox.
if (!(position is TextPointer) || ((TextPointer)position).Parent == null || ((TextPointer)position).Parent is TextBox)
{
position.InsertTextInRun(text);
}
else
{
if (!whitespacesIgnorable || text.Trim().Length > 0)
{
Run implicitRun = Inline.CreateImplicitRun(((TextPointer)position).Parent);
((TextPointer)position).InsertTextElement(implicitRun);
implicitRun.Text = text;
}
}
}
// ------------------------------------------------------------------
// Create appropriate line object.
// a) SimpleLine, if the content is represented by a string.
// b) ComplexLine, if the content is represented by a TextContainer.
// ------------------------------------------------------------------
private Line CreateLine(LineProperties lineProperties)
{
Line line;
if (_complexContent == null)
line = new SimpleLine(this, Text, lineProperties.DefaultTextRunProperties);
else
line = new ComplexLine(this);
return line;
}
// ------------------------------------------------------------------
// Make sure that complex content is enabled. Creates a default TextContainer.
// ------------------------------------------------------------------
private void EnsureComplexContent()
{
EnsureComplexContent(null);
}
// ------------------------------------------------------------------
// Make sure that complex content is enabled.
// ------------------------------------------------------------------
private void EnsureComplexContent(ITextContainer textContainer)
{
if (_complexContent == null)
{
if (textContainer == null)
{
textContainer = new TextContainer(IsContentPresenterContainer ? null : this, false /* plainTextOnly */);
}
_complexContent = new ComplexContent(this, textContainer, false, Text);
_contentCache = null;
if (CheckFlags(Flags.FormattedOnce))
{
// If we've already measured at least once, hook up the TextContainer
// listeners now.
Invariant.Assert(!CheckFlags(Flags.PendingTextContainerEventInit));
InitializeTextContainerListeners();
// Line layout data cached up to this point will become invalid
// becasue of content structure change (implicit Run added).
// So we need to clear the cache - we call InvalidateMeasure for this
// purpose. However, we do not want to produce a side effect
// of making layout invalid as a result of touching ContentStart/ContentEnd
// and other properties. For that we need to UpdateLayout when it was
// dirtied by our switch.
bool wasLayoutValid = this.IsMeasureValid && this.IsArrangeValid;
InvalidateMeasure();
InvalidateVisual(); //ensure re-rendering too
if (wasLayoutValid)
{
UpdateLayout();
}
}
else
{
// Otherwise, wait until our first measure.
// This lets us skip the work for all content invalidation
// during load, before the first measure.
SetFlags(true, Flags.PendingTextContainerEventInit);
}
}
}
// ------------------------------------------------------------------
// Make sure that complex content is cleared.
// ------------------------------------------------------------------
private void ClearComplexContent()
{
if (_complexContent != null)
{
_complexContent.Detach(this);
_complexContent = null;
Invariant.Assert(_contentCache == null, "Content cache should be null when complex content exists.");
}
}
// ------------------------------------------------------------------
// Invalidates a portion of text affected by a highlight change.
// ------------------------------------------------------------------
private void OnHighlightChanged(object sender, HighlightChangedEventArgs args)
{
Invariant.Assert(args != null);
Invariant.Assert(args.Ranges != null);
Invariant.Assert(CheckFlags(Flags.FormattedOnce), "Unexpected Highlights.Changed callback before first format!");
// The only supported highlight type for TextBlock is SpellerHightlight.
// TextSelection and HighlightComponent are ignored, because they are handled by
// separate layer.
if (args.OwnerType != typeof(SpellerHighlightLayer))
{
return;
}
// NOTE: Assuming that only rendering only properties are changeing
// through highlights.
InvalidateVisual();
}
// ------------------------------------------------------------------
// Handler for TextContainer changing notifications.
// ------------------------------------------------------------------
private void OnTextContainerChanging(object sender, EventArgs args)
{
Debug.Assert(sender == _complexContent.TextContainer, "Received text change for foreign TextContainer.");
if (CheckFlags(Flags.FormattedOnce))
{
// Will throw an exception, if during measure/arrange/render process.
VerifyTreeIsUnlocked();
// Remember the fact that content is changing.
// OnTextContainerEndChanging has to be received after this event.
SetFlags(true, Flags.ContentChangeInProgress);
}
}
// ------------------------------------------------------------------
// Handler for TextContainer changed notifications.
// ------------------------------------------------------------------
private void OnTextContainerChange(object sender, TextContainerChangeEventArgs args)
{
Invariant.Assert(args != null);
if (_complexContent == null)
{
// This shouldn't ever happen (we only hook up this handler when we have complex
// content)... except that it does happen, in cases where TextBlock is part of
// a style that gets changed in response to TextContainer.Changed events. In such a case,
// we're an obsolete text control and we don't want to do anything, so just return.
return;
}
Invariant.Assert(sender == _complexContent.TextContainer, "Received text change for foreign TextContainer.");
if (args.Count == 0)
{
// A no-op for this control. Happens when IMECharCount updates happen
// without corresponding SymbolCount changes.
return;
}
if (CheckFlags(Flags.FormattedOnce))
{
// Will throw an exception, if during measure/arrange/render process.
VerifyTreeIsUnlocked();
// Content has been changed, so reset appropriate flag.
SetFlags(false, Flags.ContentChangeInProgress);
// Invalidate measure in responce to invalidated content.
InvalidateMeasure();
}
if (!CheckFlags(Flags.TextContentChanging) && args.TextChange != TextChangeType.PropertyModified)
{
SetFlags(true, Flags.TextContentChanging);
try
{
// Use a DeferredTextReference instead of calculating the new
// value now for better performance. Most of the time no
// one cares what the new is, and loading our content into a
// string can be expensive.
SetDeferredValue(TextProperty, new DeferredTextReference(this.TextContainer));
}
finally
{
SetFlags(false, Flags.TextContentChanging);
}
}
}
private void EnsureTextBlockCache()
{
if (null == _textBlockCache)
{
_textBlockCache = new TextBlockCache();
_textBlockCache._lineProperties = GetLineProperties();
_textBlockCache._textRunCache = new TextRunCache();
}
}
// ------------------------------------------------------------------
// Refetch and cache line properties, if needed.
// ------------------------------------------------------------------
private LineProperties GetLineProperties()
{
// For default text properties always set background to null.
// REASON: If element associated with the text run is TextBlock element, ignore background
// brush, because it is handled outside as FrameworkElement's background.
TextProperties defaultTextProperties = new TextProperties(this, this.IsTypographyDefaultValue);
// Do not allow hyphenation for plain Text so always pass null for IHyphenate.
// Pass page width and height as double.MaxValue when creating LineProperties, since TextBlock does not restrict
// TextIndent or LineHeight
LineProperties lineProperties = new LineProperties(this, this, defaultTextProperties, null);
bool isHyphenationEnabled = (bool) this.GetValue(IsHyphenationEnabledProperty);
if(isHyphenationEnabled)
{
lineProperties.Hyphenator = EnsureHyphenator();
}
return lineProperties;
}
//-------------------------------------------------------------------
// Get line properties
//
// firstLine - is it for the first line?
//
// Returns: Line properties for first/following lines.
//-------------------------------------------------------------------
private TextParagraphProperties GetLineProperties(bool firstLine, LineProperties lineProperties)
{
return GetLineProperties(firstLine, false, lineProperties);
}
private TextParagraphProperties GetLineProperties(bool firstLine, bool showParagraphEllipsis, LineProperties lineProperties)
{
GetLineProperties();
firstLine = firstLine && lineProperties.HasFirstLineProperties;
if (!showParagraphEllipsis)
{
return firstLine ? lineProperties.FirstLineProps : lineProperties;
}
else
{
return lineProperties.GetParaEllipsisLineProps(firstLine);
}
}
//-------------------------------------------------------------------
// Calculate line advance distance. This functionality will go away
// when TextFormatter will be able to handle line height/stacking.
//
// lineHeight - calculated line height
//
// Returns: Line advance distance..
//-------------------------------------------------------------------
private double CalcLineAdvance(double lineHeight, LineProperties lineProperties)
{
return lineProperties.CalcLineAdvance(lineHeight);
}
//-------------------------------------------------------------------
// Calculate offset of the content taking into account horizontal / vertical
// content alignment.
//
// Returns: Content offset value.
//-------------------------------------------------------------------
private Vector CalcContentOffset(Size computedSize, double wrappingWidth)
{
Vector contentOffset = new Vector();
Thickness padding = this.Padding;
Size contentSize = new Size(Math.Max(0.0, computedSize.Width - (padding.Left + padding.Right)),
Math.Max(0.0, computedSize.Height - (padding.Top + padding.Bottom)));
switch (TextAlignment)
{
case TextAlignment.Right:
contentOffset.X = contentSize.Width - wrappingWidth;
break;
case TextAlignment.Center:
contentOffset.X = (contentSize.Width - wrappingWidth) / 2;
break;
// Default is Left alignment, in this case offset is 0.
}
contentOffset.X += padding.Left;
contentOffset.Y += padding.Top;
return contentOffset;
}
/// <summary>
/// Returns true if paragraph ellipsis will be rendered on this line
/// </summary>
/// <param name="lineIndex">
/// Index of the line
/// </param>
/// <param name="lineVOffset">
/// Vertical offset at which line starts
/// </param>
private bool ParagraphEllipsisShownOnLine(int lineIndex, double lineVOffset)
{
if (lineIndex >= LineCount - 1)
{
// Last line. No paragraph ellipses
return false;
}
// Find out if this is the last rendered line
if (!CheckFlags(Flags.HasParagraphEllipses))
{
return false;
}
// Calculate bottom offset for next line
double nextLineBottomOffset = GetLine(lineIndex + 1).Height + GetLine(lineIndex).Height + lineVOffset;
// If the next line will exceed render height by a large margin, we cannot render
// it at all and so we should show ellipsis on this one. However if the next line
// almost fits, we will render it and so there should be no ellipsis
double contentBottom = Math.Max(0.0, RenderSize.Height - Padding.Bottom);
if (DoubleUtil.GreaterThan(nextLineBottomOffset, contentBottom) && !DoubleUtil.AreClose(nextLineBottomOffset, contentBottom))
{
return true;
}
return false;
}
//-------------------------------------------------------------------
// Calculate wrapping width for lines.
//
// Returns: Wrapping width.
//-------------------------------------------------------------------
private double CalcWrappingWidth(double width)
{
// Reflowing will not happen when Width is between _previousDesiredSize.Width and ReferenceWidth.
// In some cases _previousDesiredSize.Width > ReferenceSize, use ReferenceSize in those scenarios.
if (width < _previousDesiredSize.Width)
{
width = _previousDesiredSize.Width;
}
if (width > _referenceSize.Width)
{
width = _referenceSize.Width;
}
bool usingReferenceWidth = DoubleUtil.AreClose(width, _referenceSize.Width);
double paddingWidth = Padding.Left + Padding.Right;
width = Math.Max(0.0, width - paddingWidth);
// We want FormatLine to make the same decisions it made during Measure,
// otherwise text can be truncated or trimmed when it shouldn't be.
// (Dev11 3730, 129411, 176225, 176757, 449825, 464329, 480994, 701673)
// The problem arises when the TextBox has no declared width, so its
// render size is the same as its desired size.
//
// During Measure, LineServices computes the text width in "ideal"
// coordinates (integer), TextLine converts this to "real" coordinates
// (double-precision floating-point), and TextBlock then adds the
// padding to yield the desired size. At render time (or hit-test,
// or others), this is reversed: starting with the render size,
// subtract the padding, then convert from "real" to "ideal". We want
// the result to be the same as the original text width, but there are
// two ways this can fail:
// a) in display-mode, conversion from ideal to real involves rounding
// to a multiple of the pixel size. This can cause the final
// width to fall short of the original by as much as half a pixel.
// b) (width - padding) + padding might be different from width,
// due to floating-point arithmetic error.
// In either case, if the final width is even slightly smaller than the
// original text width, LineServices might think there is not enough
// room to format the entire line.
//
// The following code protects against these errors by adjusting
// the wrapping width upward by a slight amount. But only if there
// may have been some loss.
// No adjustment is needed if we're starting with the same width
// Measure was given, or if the width is zero
if (!usingReferenceWidth && width != 0.0)
{
TextFormattingMode textFormattingMode = TextOptions.GetTextFormattingMode(this);
if (textFormattingMode == TextFormattingMode.Display)
{
// case a: rounding to pixel boundaries can lose up to half a pixel,
// as adjusted for the current DPI setting
width += 0.5 / (GetDpi().DpiScaleY);
}
if (paddingWidth != 0.0)
{
// case b: if padding is involved, add a tiny amount to
// protect against roundoff error
width += 0.00000000001;
}
}
// Make sure that TextFormatter limitations are not exceeded.
TextDpi.EnsureValidLineWidth(ref width);
return width;
}
// ------------------------------------------------------------------
// Aborts calculation by throwing exception if world has changed
// while in measure / arrange / render process.
// ------------------------------------------------------------------
private void VerifyTreeIsUnlocked()
{
if (CheckFlags(Flags.TreeInReadOnlyMode))
{
throw new InvalidOperationException(SR.Get(SRID.IllegalTreeChangeDetected));
}
}
// ------------------------------------------------------------------
// Decides if Text property needs to be serialized.
// ------------------------------------------------------------------
/// <summary>
/// This method is used by TypeDescriptor to determine if this property should
/// be serialized.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public bool ShouldSerializeText()
{
// We have to allow Text property serialization (at least for simple content)
// to preserve data-binding expressions. If we totally disable serialization
// data binding expression will be lost.
bool shouldSerialize = false;
if (_complexContent == null)
{
object localValue = ReadLocalValue(TextProperty);
if (localValue != null &&
localValue != DependencyProperty.UnsetValue &&
localValue as string != String.Empty)
{
// if the value is not null AND is not an unset value AND is EITHER an
// Expression OR a non-empty string (the last condition covers both of these
// possibilities) then we should serialize the text.
shouldSerialize = true;
}
}
return shouldSerialize;
}
// ------------------------------------------------------------------
// Decides if Inlines property needs to be serialized.
// ------------------------------------------------------------------
/// <summary>
/// This method is used by TypeDescriptor to determine if this property should
/// be serialized.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public bool ShouldSerializeInlines(XamlDesignerSerializationManager manager)
{
return (_complexContent != null) && (manager != null) && (manager.XmlWriter == null);
}
// ------------------------------------------------------------------
// Do content alignment.
// ------------------------------------------------------------------
private void AlignContent()
{
Debug.Assert(IsLayoutDataValid);
Debug.Assert(CheckFlags(Flags.RequiresAlignment));
// Line props may be invalid, even if Measure/Arrange is valid - rendering only props are changing.
LineProperties lineProperties = GetLineProperties();
double wrappingWidth = CalcWrappingWidth(RenderSize.Width);
Vector contentOffset = CalcContentOffset(RenderSize, wrappingWidth);
// Create / format all lines.
// Since we are disposing line object, it can be reused to format following lines.
Line line = CreateLine(lineProperties);
TextRunCache textRunCache = new TextRunCache();
int dcp = 0;
double lineOffset = 0;
int lineCount = LineCount;
for (int i = 0; i < lineCount; i++)
{
Debug.Assert(lineCount == LineCount);
LineMetrics lineMetrics = GetLine(i);
using (line)
{
bool ellipsis = ParagraphEllipsisShownOnLine(i, lineOffset);
line.Format(dcp, wrappingWidth, GetLineProperties(dcp == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, ellipsis);
double lineHeight = CalcLineAdvance(line.Height, lineProperties);
// Check consistency of line formatting
MS.Internal.Invariant.Assert(lineMetrics.Length == line.Length, "Line length is out of sync");
Debug.Assert(DoubleUtil.AreClose(lineHeight, lineMetrics.Height), "Line height is out of sync.");
// Calculated line width might be different from measure width in following cases:
// a) dynamically sized children, when FinalSize != AvailableSize
// b) non-default horizontal alignment, when FinalSize != AvailableSize
// Hence do not assert about matching line width with cached line metrics.
lineMetrics = UpdateLine(i, lineMetrics, line.Start, line.Width);
dcp += lineMetrics.Length;
lineOffset += lineHeight;
}
}
SetFlags(false, Flags.RequiresAlignment);
}
// ------------------------------------------------------------------
// OnRequestBringIntoView is called from the event handler TextBlock
// registers for the event.
// Handle the event for hosted ContentElements, and raise a new BringIntoView
// event with the following values:
// * object: (this)
// * rect: A rect indicating the position of the ContentElement
//
// sender - The instance handling the event.
// args - RequestBringIntoViewEventArgs indicates the element
// and region to scroll into view.
// ------------------------------------------------------------------
private static void OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs args)
{
TextBlock textBlock = sender as TextBlock;
ContentElement child = args.TargetObject as ContentElement;
if (textBlock != null && child != null)
{
if (TextBlock.ContainsContentElement(textBlock, child))
{
// Handle original event.
args.Handled = true;
// Retrieve the first rectangle representing the child and
// raise a new BrightIntoView event with such rectangle.
ReadOnlyCollection<Rect> rects = textBlock.GetRectanglesCore(child);
Invariant.Assert(rects != null, "Rect collection cannot be null.");
if (rects.Count > 0)
{
textBlock.BringIntoView(rects[0]);
}
else
{
textBlock.BringIntoView();
}
}
}
}
private static bool ContainsContentElement(TextBlock textBlock, ContentElement element)
{
if (textBlock._complexContent == null || !(textBlock._complexContent.TextContainer is TextContainer))
{
return false;
}
else if (element is TextElement)
{
if (textBlock._complexContent.TextContainer != ((TextElement)element).TextContainer)
{
return false;
}
else
{
return true;
}
}
return false;
}
private int LineCount
{
get
{
if (CheckFlags(Flags.HasFirstLine))
{
return (_subsequentLines == null) ? 1 : _subsequentLines.Count + 1;
}
return 0;
}
}
private LineMetrics GetLine(int index)
{
return (index == 0) ? _firstLine : _subsequentLines[index - 1];
}
private LineMetrics UpdateLine(int index, LineMetrics metrics, double start, double width)
{
metrics = new LineMetrics(metrics, start, width);
if (index == 0)
{
_firstLine = metrics;
}
else
{
_subsequentLines[index - 1] = metrics;
}
return metrics;
}
// ------------------------------------------------------------------
// SetFlags is used to set or unset one or multiple flags.
// ------------------------------------------------------------------
private void SetFlags(bool value, Flags flags)
{
_flags = value ? (_flags | flags) : (_flags & (~flags));
}
// ------------------------------------------------------------------
// CheckFlags returns true if all of passed flags in the bitmask are set.
// ------------------------------------------------------------------
private bool CheckFlags(Flags flags)
{
return ((_flags & flags) == flags);
}
// ------------------------------------------------------------------
// Ensures none of our public (or textview) methods can be called during measure/arrange/content change.
// ------------------------------------------------------------------
private void VerifyReentrancy()
{
if(CheckFlags(Flags.MeasureInProgress))
{
throw new InvalidOperationException(SR.Get(SRID.MeasureReentrancyInvalid));
}
if(CheckFlags(Flags.ArrangeInProgress))
{
throw new InvalidOperationException(SR.Get(SRID.ArrangeReentrancyInvalid));
}
if(CheckFlags(Flags.ContentChangeInProgress))
{
throw new InvalidOperationException(SR.Get(SRID.TextContainerChangingReentrancyInvalid));
}
}
/// <summary>
/// Returns index of the line that starts at the given dcp. Returns -1 if
/// no line or the line metrics collection starts at the given dcp
/// </summary>
/// <param name="dcpLine">
/// Start dcp of required line
/// </param>
private int GetLineIndexFromDcp(int dcpLine)
{
Invariant.Assert(dcpLine >= 0);
int lineIndex = 0;
int lineStartOffset = 0;
int lineCount = LineCount;
while (lineIndex < lineCount)
{
Debug.Assert(lineCount == LineCount);
if (lineStartOffset == dcpLine)
{
// Found line that starts at given dcp
return lineIndex;
}
else
{
lineStartOffset += GetLine(lineIndex).Length;
++lineIndex;
}
}
// No line found starting at this position. Return -1.
// We should never hit this code
Invariant.Assert(false, "Dcp passed is not at start of any line in TextBlock");
return -1;
}
// ------------------------------------------------------------------
// IContentHost Helpers
// ------------------------------------------------------------------
/// <summary>
/// Searches for an element in the _complexContent.TextContainer. If the element is found, returns the
/// position at which it is found. Otherwise returns null.
/// </summary>
/// <param name="e">
/// Element to be found.
/// </param>
/// <remarks>
/// We assume that this function is called from within text if the caller knows that _complexContent exists
/// and contains a TextContainer. Hence we assert for this condition within the function
/// </remarks>
private TextPointer FindElementPosition(IInputElement e)
{
// Parameter validation
Debug.Assert(e != null);
// Validate that this function is only called when a TextContainer exists as complex content
Debug.Assert(_complexContent.TextContainer is TextContainer);
TextPointer position;
// If e is a TextElement we can optimize by checking its TextContainer
if (e is TextElement)
{
if ((e as TextElement).TextContainer == _complexContent.TextContainer)
{
// Element found
position = new TextPointer((e as TextElement).ElementStart);
return position;
}
}
// Else: search for e in the complex content
position = new TextPointer((TextPointer)_complexContent.TextContainer.Start);
while (position.CompareTo((TextPointer)_complexContent.TextContainer.End) < 0)
{
// Search each position in _complexContent.TextContainer for the element
switch (position.GetPointerContext(LogicalDirection.Forward))
{
case TextPointerContext.EmbeddedElement:
DependencyObject embeddedObject = position.GetAdjacentElement(LogicalDirection.Forward);
if (embeddedObject is ContentElement || embeddedObject is UIElement)
{
if (embeddedObject == e as ContentElement || embeddedObject == e as UIElement)
{
return position;
}
}
break;
default:
break;
}
position.MoveByOffset(+1);
}
// Reached end of complex content without finding the element
return null;
}
/// <summary>
/// Called when the child's BaselineOffset value changes.
/// </summary>
internal void OnChildBaselineOffsetChanged(DependencyObject source)
{
// Ignore this notification, if currently in the measure process.
if (!CheckFlags(Flags.MeasureInProgress))
{
// BaselineOffset, may affect the
// size. Hence invalidate measure.
// There is no need to invalidate TextRunCache, since TextFormatter
// regets inline object information even if TextRunCache is clean.
InvalidateMeasure();
InvalidateVisual(); //ensure re-rendering
}
}
/// <summary>
/// Property invalidator for baseline offset
/// </summary>
/// <param name="d">Dependency Object that the property value is being changed on.</param>
/// <param name="e">EventArgs that contains the old and new values for this property</param>
private static void OnBaselineOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
//Set up our baseline changed event
//fire event!
TextElement te = TextElement.ContainerTextElementField.GetValue(d);
if (te != null)
{
DependencyObject parent = te.TextContainer.Parent;
TextBlock tb = parent as TextBlock;
if (tb != null)
{
tb.OnChildBaselineOffsetChanged(d);
}
else
{
FlowDocument fd = parent as FlowDocument;
if (fd != null && d is UIElement)
{
fd.OnChildDesiredSizeChanged((UIElement)d);
}
}
}
}
// ------------------------------------------------------------------
// Setup event handlers.
// Deferred until the first measure.
// ------------------------------------------------------------------
private void InitializeTextContainerListeners()
{
_complexContent.TextContainer.Changing += new EventHandler(OnTextContainerChanging);
_complexContent.TextContainer.Change += new TextContainerChangeEventHandler(OnTextContainerChange);
_complexContent.Highlights.Changed += new HighlightChangedEventHandler(OnHighlightChanged);
}
// ------------------------------------------------------------------
// Clears out line metrics array, disposes as appropriate
// ------------------------------------------------------------------
private void ClearLineMetrics()
{
if (CheckFlags(Flags.HasFirstLine))
{
if (_subsequentLines != null)
{
int subsequentLineCount = _subsequentLines.Count;
for (int i = 0; i < subsequentLineCount; i++)
{
_subsequentLines[i].Dispose(false);
}
_subsequentLines = null;
}
_firstLine = _firstLine.Dispose(true);
SetFlags(false, Flags.HasFirstLine);
}
}
// ------------------------------------------------------------------
// Ensures the hyphenator object is created
// ------------------------------------------------------------------
private NaturalLanguageHyphenator EnsureHyphenator()
{
if (CheckFlags(Flags.IsHyphenatorSet))
{
return HyphenatorField.GetValue(this);
}
// initialize hyphenator
NaturalLanguageHyphenator hyphenator = new NaturalLanguageHyphenator();
HyphenatorField.SetValue(this, hyphenator);
SetFlags(true, Flags.IsHyphenatorSet);
return hyphenator;
}
private static bool IsValidTextTrimming(object o)
{
TextTrimming value = (TextTrimming)o;
return value == TextTrimming.CharacterEllipsis
|| value == TextTrimming.None
|| value == TextTrimming.WordEllipsis;
}
private static bool IsValidTextWrap(object o)
{
TextWrapping value = (TextWrapping)o;
return value == TextWrapping.Wrap
|| value == TextWrapping.NoWrap
|| value == TextWrapping.WrapWithOverflow;
}
#endregion Private methods
//-------------------------------------------------------------------
//
// Private Fields
//
//-------------------------------------------------------------------
#region Private Fields
//------------------------------------------------------------------
// Simple content.
//-------------------------------------------------------------------
private TextBlockCache _textBlockCache;
//------------------------------------------------------------------
// Simple content.
//-------------------------------------------------------------------
private string _contentCache;
//-------------------------------------------------------------------
// Complex content.
//-------------------------------------------------------------------
private ComplexContent _complexContent;
//-------------------------------------------------------------------
// Text formatter object.
//-------------------------------------------------------------------
private TextFormatter _textFormatterIdeal;
//-------------------------------------------------------------------
// Text formatter object.
//-------------------------------------------------------------------
private TextFormatter _textFormatterDisplay;
//-------------------------------------------------------------------
// This size was the most recent constraint passed to MeasureOverride.
// this.PreviousConstraintcan not be used because it can be affected
// by Margin, Width/Min/MaxWidth propreties and ClipToBounds...
//-------------------------------------------------------------------
private Size _referenceSize;
//-------------------------------------------------------------------
// This size was a result returned by MeasureOverride last time.
// this.DesiredSize can not be used because it can be affected by Margin,
// Width/Min/MaxWidth propreties and ClipToBounds...
//-------------------------------------------------------------------
private Size _previousDesiredSize;
//-------------------------------------------------------------------
// Distance from the top of the Element to its baseline.
//-------------------------------------------------------------------
private double _baselineOffset;
//-------------------------------------------------------------------
// Hyphenator -- uncommon field given usage
//-------------------------------------------------------------------
private static readonly UncommonField<NaturalLanguageHyphenator> HyphenatorField = new UncommonField<NaturalLanguageHyphenator>();
//-------------------------------------------------------------------
// Collection of metrics of each line. For performance reasons, we
// have separated out the first line from the array, since a single
// line of text is a very common usage. The LineCount, GetLine,
// and UpdateLine members are used to simplify access to this
// divided data structure.
//-------------------------------------------------------------------
private LineMetrics _firstLine;
private List<LineMetrics> _subsequentLines;
//-------------------------------------------------------------------
// Flags reflecting various aspects of TextBlock's state.
//-------------------------------------------------------------------
private Flags _flags;
[System.Flags]
private enum Flags
{
FormattedOnce = 0x1, // Element has been formatted at least once.
MeasureInProgress = 0x2, // Measure is in progress.
TreeInReadOnlyMode = 0x4, // Tree (content) is in read only mode.
RequiresAlignment = 0x8, // Content requires alignment process.
ContentChangeInProgress = 0x10, // Content change is in progress
//(it has been started, but is is not completed yet).
IsContentPresenterContainer = 0x20, // Is this Text control being used by a ContentPresenter to host its content
HasParagraphEllipses = 0x40, // Has paragraph ellipses
PendingTextContainerEventInit = 0x80, // Needs TextContainer event hookup on next Measure call.
ArrangeInProgress = 0x100, // Arrange is in progress.
IsTypographySet = 0x200, // Typography properties are not at default values
TextContentChanging = 0x400, // TextProperty update in progress.
IsHyphenatorSet = 0x800, // used to indicate when HyphenatorField has been set
HasFirstLine = 0x1000,
}
#endregion Private Fields
//-------------------------------------------------------------------
//
// Private Types
//
//-------------------------------------------------------------------
#region Private Types
//-------------------------------------------------------------------
// Represents complex content.
//-------------------------------------------------------------------
private class ComplexContent
{
//---------------------------------------------------------------
// Ctor
//---------------------------------------------------------------
internal ComplexContent(TextBlock owner, ITextContainer textContainer, bool foreignTextContianer, string content)
{
// Paramaters validation
Debug.Assert(owner != null);
Debug.Assert(textContainer != null);
VisualChildren = new VisualCollection(owner);
// Store TextContainer that contains content of the element.
TextContainer = textContainer;
ForeignTextContainer = foreignTextContianer;
// Add content
if (content != null && content.Length > 0)
{
TextBlock.InsertTextRun(this.TextContainer.End, content, /*whitespacesIgnorable:*/false);
}
// Create TextView associated with TextContainer.
this.TextView = new TextParagraphView(owner, TextContainer);
// Hookup TextContainer to TextView.
this.TextContainer.TextView = this.TextView;
}
//---------------------------------------------------------------
// Detach event handlers.
//---------------------------------------------------------------
internal void Detach(TextBlock owner)
{
this.Highlights.Changed -= new HighlightChangedEventHandler(owner.OnHighlightChanged);
this.TextContainer.Changing -= new EventHandler(owner.OnTextContainerChanging);
this.TextContainer.Change -= new TextContainerChangeEventHandler(owner.OnTextContainerChange);
}
//------------------------------------------------------------------
// Internal Visual Children.
//-------------------------------------------------------------------
internal VisualCollection VisualChildren;
//---------------------------------------------------------------
// Highlights associated with TextContainer.
//---------------------------------------------------------------
internal Highlights Highlights { get { return this.TextContainer.Highlights; } }
//---------------------------------------------------------------
// Text array exposing access to the content.
//---------------------------------------------------------------
internal readonly ITextContainer TextContainer;
//---------------------------------------------------------------
// Is TextContainer owned by another object?
//---------------------------------------------------------------
internal readonly bool ForeignTextContainer;
//---------------------------------------------------------------
// TextView object associated with TextContainer.
//---------------------------------------------------------------
internal readonly TextParagraphView TextView;
//---------------------------------------------------------------
// Collection of inline objects hosted by the TextBlock control.
//---------------------------------------------------------------
internal ArrayList InlineObjects;
}
//-------------------------------------------------------------------
// Simple content enumerator.
//-------------------------------------------------------------------
private class SimpleContentEnumerator : IEnumerator
{
//---------------------------------------------------------------
// Ctor
//---------------------------------------------------------------
internal SimpleContentEnumerator(string content)
{
_content = content;
_initialized = false;
_invalidPosition = false;
}
//---------------------------------------------------------------
// Sets the enumerator to its initial position, which is before
// the first element in the collection.
//---------------------------------------------------------------
void IEnumerator.Reset()
{
_initialized = false;
_invalidPosition = false;
}
//---------------------------------------------------------------
// Advances the enumerator to the next element of the collection.
//---------------------------------------------------------------
bool IEnumerator.MoveNext()
{
if (!_initialized)
{
_initialized = true;
return true;
}
else
{
_invalidPosition = true;
return false;
}
}
//---------------------------------------------------------------
// Gets the current element in the collection.
//---------------------------------------------------------------
object IEnumerator.Current
{
get
{
if (!_initialized || _invalidPosition)
{
throw new InvalidOperationException();
}
return _content;
}
}
//---------------------------------------------------------------
// Content.
//---------------------------------------------------------------
private readonly string _content;
private bool _initialized;
private bool _invalidPosition;
}
#endregion Private Types
//-------------------------------------------------------------------
//
// Dependency Property Helpers
//
//-------------------------------------------------------------------
#region Dependency Property Helpers
private static object CoerceBaselineOffset(DependencyObject d, object value)
{
TextBlock tb = (TextBlock) d;
if(DoubleUtil.IsNaN((double) value))
{
return tb._baselineOffset;
}
return value;
}
/// <summary>
/// Returns true if we should serialize the BaselineOffset property.
/// </summary>
/// <returns>True if we should serialize the BaselineOffset property.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public bool ShouldSerializeBaselineOffset()
{
object localBaseline = ReadLocalValue(BaselineOffsetProperty);
return (localBaseline != DependencyProperty.UnsetValue) && !DoubleUtil.IsNaN((double) localBaseline);
}
//-------------------------------------------------------------------
// Text helpers
//-------------------------------------------------------------------
private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
OnTextChanged(d, (string)e.NewValue);
}
private static void OnTextChanged(DependencyObject d, string newText)
{
TextBlock text = (TextBlock) d;
if (text.CheckFlags(Flags.TextContentChanging))
{
// The update originated in a TextContainer change -- don't update
// the TextContainer a second time.
return;
}
if (text._complexContent == null)
{
text._contentCache = (newText != null) ? newText : String.Empty;
}
else
{
text.SetFlags(true, Flags.TextContentChanging);
try
{
bool exceptionThrown = true;
Invariant.Assert(text._contentCache == null, "Content cache should be null when complex content exists.");
text._complexContent.TextContainer.BeginChange();
try
{
((TextContainer)text._complexContent.TextContainer).DeleteContentInternal((TextPointer)text._complexContent.TextContainer.Start, (TextPointer)text._complexContent.TextContainer.End);
InsertTextRun(text._complexContent.TextContainer.End, newText, /*whitespacesIgnorable:*/true);
exceptionThrown = false;
}
finally
{
text._complexContent.TextContainer.EndChange();
if (exceptionThrown)
{
text.ClearLineMetrics();
}
}
}
finally
{
text.SetFlags(false, Flags.TextContentChanging);
}
}
}
//
// This property
// 1. Finds the correct initial size for the _effectiveValues store on the current DependencyObject
// 2. This is a performance optimization
//
internal override int EffectiveValuesInitialSize
{
get { return 28; }
}
#endregion Dependency Property Helpers
}
}
// Disable pragma warnings to enable PREsharp pragmas
#pragma warning restore 1634, 1691
|