File: Microsoft\Windows\Controls\Ribbon\RibbonTwoLineText.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\System.Windows.Controls.Ribbon\System.Windows.Controls.Ribbon_dxtfdo3u_wpftmp.csproj (System.Windows.Controls.Ribbon)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#region Using declarations
 
using System.ComponentModel;
using System.Diagnostics;
using System.Windows.Automation.Peers;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Shapes;
#if RIBBON_IN_FRAMEWORK
using Microsoft.Windows.Controls;
 
#if RIBBON_IN_FRAMEWORK
namespace System.Windows.Controls.Ribbon
#else
namespace Microsoft.Windows.Controls.Ribbon
#endif
{
#else
    using Microsoft.Windows.Automation.Peers;
#endif
 
    #endregion
 
    /// <summary>
    ///   A specialized label that can lay itself out on at most two lines, and
    ///   also has the ability to display a special Geometry inline with its text.
    /// </summary>
    [TemplatePart(Name = "PART_TextBlock1", Type = typeof(TextBlock))]
    [TemplatePart(Name = "PART_TextBlock2", Type = typeof(TextBlock))]
    [TemplatePart(Name = "PART_Path", Type = typeof(Path))]
    public class RibbonTwoLineText : Control
    {
        #region Constructors
        
        static RibbonTwoLineText()
        {
            Type ownerType = typeof(RibbonTwoLineText);
 
            DefaultStyleKeyProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(ownerType));
            FocusableProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(false));
        }
 
        #endregion
 
        #region Fields
 
        private TextBlock _textBlock1, _textBlock2;
        private Path _path;
 
        #endregion
 
        #region Public Properties
 
        public TextDecorationCollection TextDecorations
        {
            get { return (TextDecorationCollection)GetValue(TextDecorationsProperty); }
            set { SetValue(TextDecorationsProperty, value); }
        }
 
        public static readonly DependencyProperty TextDecorationsProperty = TextBlock.TextDecorationsProperty.AddOwner(typeof(RibbonTwoLineText));
 
        public TextEffectCollection TextEffects
        {
            get { return (TextEffectCollection)GetValue(TextEffectsProperty); }
            set { SetValue(TextEffectsProperty, value); }
        }
 
        public static readonly DependencyProperty TextEffectsProperty = TextBlock.TextEffectsProperty.AddOwner(typeof(RibbonTwoLineText));
        
        public double BaselineOffset
        {
            get { return (double)GetValue(BaselineOffsetProperty); }
            set { SetValue(BaselineOffsetProperty, value); }
        }
 
        public static readonly DependencyProperty BaselineOffsetProperty = TextBlock.BaselineOffsetProperty.AddOwner(typeof(RibbonTwoLineText));
        
        new public Thickness Padding
        {
            get { return (Thickness)GetValue(PaddingProperty); }
            set { SetValue(PaddingProperty, value); }
        }
 
        new public static readonly DependencyProperty PaddingProperty = TextBlock.PaddingProperty.AddOwner(typeof(RibbonTwoLineText));
 
        /// <summary>
        ///   Gets or sets the TextAlignment set on the internal text block.
        /// </summary>
        public TextAlignment TextAlignment
        {
            get { return (TextAlignment)GetValue(TextAlignmentProperty); }
            set { SetValue(TextAlignmentProperty, value); }
        }
 
        /// <summary>
        ///   Adds RibbonTwoLineText as an owner of this property.
        /// </summary>
        public static readonly DependencyProperty TextAlignmentProperty =
            TextBlock.TextAlignmentProperty.AddOwner(typeof(RibbonTwoLineText));
 
        /// <summary>
        ///   Gets or sets the TextTrimming set on the internal text block.
        /// </summary>
        public TextTrimming TextTrimming
        {
            get { return (TextTrimming)GetValue(TextTrimmingProperty); }
            set { SetValue(TextTrimmingProperty, value); }
        }
 
        /// <summary>
        ///   Adds RibbonTwoLineText as an owner of this property.
        /// </summary>
        public static readonly DependencyProperty TextTrimmingProperty =
            TextBlock.TextTrimmingProperty.AddOwner(typeof(RibbonTwoLineText));
 
 
        /// <summary>
        ///   Gets or sets the LineHeight set on the internal text blocks.
        /// </summary>
        [TypeConverter(typeof(LengthConverter))]
        public double LineHeight
        {
            get { return (double)GetValue(LineHeightProperty); }
            set { SetValue(LineHeightProperty, value); }
        }
 
        /// <summary>
        ///   Adds RibbonTwoLineText as an owner of this property.
        /// </summary>
        public static readonly DependencyProperty LineHeightProperty =
            TextBlock.LineHeightProperty.AddOwner(typeof(RibbonTwoLineText));
        
        /// <summary>
        ///   Gets or sets the LineStackingStrategy set on the internal text blocks.
        /// </summary>
        public LineStackingStrategy LineStackingStrategy
        {
            get { return (LineStackingStrategy)GetValue(LineStackingStrategyProperty); }
            set { SetValue(LineStackingStrategyProperty, value); }
        }
 
        /// <summary>
        ///   Adds RibbonTwoLineText as an owner of this property.
        /// </summary>
        public static readonly DependencyProperty LineStackingStrategyProperty =
            TextBlock.LineStackingStrategyProperty.AddOwner(typeof(RibbonTwoLineText));
 
        /// <summary>
        ///   Gets or sets the fill of the label's appending geometry.
        /// </summary>
        public Brush PathFill
        {
            get { return (Brush)GetValue(PathFillProperty); }
            set { SetValue(PathFillProperty, value); }
        }
 
        /// <summary>
        ///   Using a DependencyProperty as the backing store for PathFillProperty.  This enables animation, styling, binding, etc...
        /// </summary>
        public static readonly DependencyProperty PathFillProperty =
                    DependencyProperty.Register(
                            "PathFill",
                            typeof(Brush),
                            typeof(RibbonTwoLineText),
                            null);
 
        /// <summary>
        ///   Gets or sets the fill of the label's appending geometry.
        /// </summary>
        public Brush PathStroke
        {
            get { return (Brush)GetValue(PathStrokeProperty); }
            set { SetValue(PathStrokeProperty, value); }
        }
 
        /// <summary>
        ///   Using a DependencyProperty as the backing store for PathStrokeProperty.  This enables animation, styling, binding, etc...
        /// </summary>
        public static readonly DependencyProperty PathStrokeProperty =
                    DependencyProperty.Register(
                            "PathStroke",
                            typeof(Brush),
                            typeof(RibbonTwoLineText),
                            null);
 
        /// <summary>
        ///   Gets or sets the label's text.
        /// </summary>
        public string Text
        {
            get { return (string)GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }
 
        /// <summary>
        ///   Using a DependencyProperty as the backing store for TextProperty.  This enables animation, styling, binding, etc...
        /// </summary>
        public static readonly DependencyProperty TextProperty =
                    TextBlock.TextProperty.AddOwner(typeof(RibbonTwoLineText), 
                    new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsMeasure));
 
        /// <summary>
        ///   Defines Path data for the label's appending Geometry.
        /// </summary>
        public static readonly DependencyProperty PathDataProperty =
                    DependencyProperty.RegisterAttached(
                            "PathData",
                            typeof(Geometry),
                            typeof(RibbonTwoLineText),
                            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender));
 
        public static Geometry GetPathData(DependencyObject element)
        {
            ArgumentNullException.ThrowIfNull(element);
            return (Geometry)element.GetValue(PathDataProperty);
        }
 
        public static void SetPathData(DependencyObject element, Geometry value)
        {
            ArgumentNullException.ThrowIfNull(element);
            element.SetValue(PathDataProperty, value);
        }
 
        /// <summary>
        ///   Using a DependencyProperty as the backing store for HasTwoLinesProperty.  This enables animation, styling, binding, etc...
        /// </summary>
        public static readonly DependencyProperty HasTwoLinesProperty
            = DependencyProperty.RegisterAttached("HasTwoLines",
                                                    typeof(bool),
                                                    typeof(RibbonTwoLineText),
                                                    new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsMeasure));
        
        /// <summary>
        ///   Gets or sets a value indicating whether the RibbonTwoLineText should be allowed to
        ///   layout on two lines or if it should be restricted to one.
        /// </summary>
        public static bool GetHasTwoLines(DependencyObject element)
        {
            ArgumentNullException.ThrowIfNull(element);
            return (bool)element.GetValue(HasTwoLinesProperty);
        }
 
        public static void SetHasTwoLines(DependencyObject element, bool value)
        {
            ArgumentNullException.ThrowIfNull(element);
            element.SetValue(HasTwoLinesProperty, value);
        }
 
        #endregion
 
        #region Protected Methods
 
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            _textBlock1 = this.GetTemplateChild("PART_TextBlock1") as TextBlock;
            _textBlock2 = this.GetTemplateChild("PART_TextBlock2") as TextBlock;
            _path = this.GetTemplateChild("PART_Path") as Path;
        }
 
        protected override Size MeasureOverride(Size availableSize)
        {
            if (_textBlock1 != null && _textBlock2 != null && _path != null)
            {
                bool hasTwoLines = GetHasTwoLines(this);
                if (!hasTwoLines)
                {
                    // Collapse first line
                    _textBlock1.Text = null;
                    _textBlock1.Visibility = Visibility.Collapsed;
 
                    // All text shown in second line
                    _textBlock2.Text = this.Text;
                    _textBlock2.Visibility = Visibility.Visible;
 
                    // Mark measure dirty all UIElements in the visual tree from _textBlock2 upto RibbonTwoLineText
                    TreeHelper.InvalidateMeasureForVisualAncestorPath<RibbonTwoLineText>(_textBlock2);
                }
                else   // HasTwoLines
                {
                    // Querying the ContentStart property for the very first time has a potential of invalidating 
                    // measure on the TextBlock. So we want to do this once before we call Measure and Arrange to 
                    // guarantee valid layout
                    TextPointer secondLinePointer = _textBlock1.ContentStart;
 
                    // Determine LineBreak with availableSize
                    MeasureWithConstraint(availableSize, out secondLinePointer);
 
                    if (secondLinePointer == null)
                    {
                        MeasureWithoutConstraint(out secondLinePointer);
                    }
 
                    if (secondLinePointer != null)
                    {
                        // We need to split Text manually
                        string firstLineText = secondLinePointer.GetTextInRun(LogicalDirection.Backward);
                        string secondLineText = secondLinePointer.GetTextInRun(LogicalDirection.Forward);
 
                        // Display first line in full 
                        _textBlock1.Text = firstLineText;
                        _textBlock1.Visibility = Visibility.Visible;
 
                        // remaining text in second line. Trimming will be applied if TextTrimming is set,
                        // or else extra text will be clipped.
                        _textBlock2.Text = secondLineText;
                        _textBlock2.Visibility = Visibility.Visible;
 
                        // Mark measure dirty all UIElements in the visual tree from _textBlock2 upto RibbonTwoLineText
                        TreeHelper.InvalidateMeasureForVisualAncestorPath<RibbonTwoLineText>(_textBlock2);
                    }
                    else
                    {
                        // Text is small enough to be displayed in first line alone
                        _textBlock1.Text = this.Text;
                        _textBlock1.Visibility = Visibility.Visible;
 
                        // Collapse second line
                        _textBlock2.Text = null;
                        _textBlock2.Visibility = Visibility.Collapsed;
                    }
                }
            }
            return base.MeasureOverride(availableSize);
        }
        
        /// <summary>
        /// Layout Text into two lines when width is restricted and determine if a LineBreak occurs.
        /// </summary>
        /// <param name="availableSize">constraint</param>
        /// <param name="secondLinePointer">TextPointer to where a LineBreak occurs</param>
        private void MeasureWithConstraint(Size availableSize, out TextPointer secondLinePointer)
        {
            Debug.Assert(_textBlock1 != null && _textBlock2 != null);
 
            // Temporarily assign _textBlock1.Text to measure the length it would require
            _textBlock1.Text = this.Text;
            _textBlock1.TextWrapping = TextWrapping.WrapWithOverflow;
            _textBlock1.Visibility = Visibility.Visible;
 
            secondLinePointer = null;
 
            _textBlock1.Measure(availableSize);
 
            // Dev11 Bug 65031 - Text: Invariant Assert hit in TextBlock due to loss of precision error
            // Under very rare cases, a loss of precision due to the UseLayoutRounding feature may
            // occur on 64-bit systems that can mean the difference between rounding and not.
            //
            // To work around this, we augment up _textBlock1's DesiredSize.Width by 0.5 pixels
            // before arranging it.  This is safe, because this measure/arrange pass justs determines
            // what should go on the second line of the TwoLineText and is not the actual measure/arrange.
            Size bufferedSize = new Size(_textBlock1.DesiredSize.Width + 0.5,
                                         _textBlock1.DesiredSize.Height);
            _textBlock1.Arrange(new Rect(bufferedSize));
 
            secondLinePointer = _textBlock1.ContentStart.GetLineStartPosition(1);
 
            // Clear local value
            _textBlock1.ClearValue(TextBlock.TextWrappingProperty);
        }
 
        /// <summary>
        /// Layout Text into two lines without any constraint and determine if a LineBreak occurs.
        /// </summary>
        /// <param name="secondLinePointer">TextPointer to where a LineBreak occurs</param>
        private void MeasureWithoutConstraint(out TextPointer secondLinePointer)
        {
            Debug.Assert(_textBlock1 != null && _textBlock2 != null);
 
            secondLinePointer = null;
 
            // Temporarily assign _textBlock1.Text to measure the length it would require
            _textBlock1.Text = this.Text;
            _textBlock1.TextWrapping = TextWrapping.WrapWithOverflow;
            _textBlock1.Visibility = Visibility.Visible;
            
            // This measures the TextBlock with infinite bounds to see what its ideal
            // (one-line) Width would be.  If this width is greater than the minimum
            // width for the label, we try halving its site to
            // spread the label to two lines.
            Size infinity = new Size(double.PositiveInfinity, double.PositiveInfinity);
            _textBlock1.Measure(infinity);
            
            double width = _textBlock1.DesiredSize.Width;
            int wordCount = string.IsNullOrEmpty(Text) ? 0 :  Text.Split().Length;
 
            if ( wordCount > 1)
            {
                // Set the TextBlock's width to half of its desired with to spread it
                // to two lines, and re-run layout.
                _textBlock1.Width = Math.Max(1.0, width / 2.0);
                _textBlock1.Measure(infinity);
                _textBlock1.Arrange(new Rect(_textBlock1.DesiredSize));
 
                // If text flows into more than 2 lines
                TextPointer currentLinePointer = _textBlock1.ContentStart.GetLineStartPosition(2);
                if (currentLinePointer != null)
                {
                    double extraLength = 0.0;
                    Rect lastCharacter, firstCharacter;
 
                    // Iterate through 3rd to (n-1)th line and add up their lengths.
                    TextPointer nextLinePointer = currentLinePointer.GetLineStartPosition(1);
                    while (nextLinePointer != null)
                    {
                        lastCharacter = nextLinePointer.GetCharacterRect(LogicalDirection.Backward);
                        firstCharacter = currentLinePointer.GetCharacterRect(LogicalDirection.Forward);
                        extraLength += Math.Abs(lastCharacter.X - firstCharacter.X);
                        currentLinePointer = nextLinePointer;
                        nextLinePointer = nextLinePointer.GetLineStartPosition(1);
                    }
 
                    // Add length of last line
                    lastCharacter = _textBlock1.ContentEnd.GetCharacterRect(LogicalDirection.Backward);
                    firstCharacter = currentLinePointer.GetCharacterRect(LogicalDirection.Forward);
                    extraLength += Math.Abs(lastCharacter.X - firstCharacter.X);
 
                    // Redistribute the extraLength among first two lines
                    _textBlock1.Width = _textBlock1.Width + extraLength / 2;
                    _textBlock1.Measure(infinity);
                    _textBlock1.Arrange(new Rect(_textBlock1.DesiredSize));
                }
                
                secondLinePointer = _textBlock1.ContentStart.GetLineStartPosition(1);
                
                // Clear the temporarily assigned values
                _textBlock1.ClearValue(TextBlock.WidthProperty);
            }
 
            _textBlock1.ClearValue(TextBlock.TextWrappingProperty);
        }
 
        protected override AutomationPeer OnCreateAutomationPeer()
        {
            return new RibbonTwoLineTextAutomationPeer(this);
        }
 
        #endregion
    }
}