File: System\Windows\Documents\CompositionAdorner.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\PresentationFramework\PresentationFramework.csproj (PresentationFramework)
// 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.
 
//
// Description: Composition adorner to render the composition display attribute.
// 
 
namespace System.Windows.Documents
{
    using System.Collections; // ArrayList
    using System.Diagnostics;
    using System.Windows.Media; // Brush, Transform
    using System.Windows.Controls; // TextBox
    using System.Windows.Controls.Primitives; // TextBoxBase
    using System.Windows.Input; // InputLanguageManager
    using System.Windows.Threading; // Dispatcher
    using MS.Win32;             // TextServices
    using MS.Internal; // Invariant
 
    internal class CompositionAdorner : Adorner
    {
        //------------------------------------------------------
        //
        //  Constructors
        //
        //------------------------------------------------------
 
        #region Constructors
 
        static CompositionAdorner()
        {
            // Provide a new default value for the composition adorner so that it is not hit-testable.
            IsEnabledProperty.OverrideMetadata(typeof(CompositionAdorner), new FrameworkPropertyMetadata(false));
        }
        
        /// <summary>
        /// Creates new instance of CompositionAdorner.
        /// </summary>
        /// <param name="textView">
        /// TextView to which this CompositionAdorner is attached as adorner.
        /// </param>
        internal CompositionAdorner(ITextView textView) : this(textView, new ArrayList())
        {
        }
 
        /// <summary>
        /// Creates new instance of CompositionAdorner.
        /// </summary>
        /// <param name="textView">
        /// TextView to which this CompositionAdorner is attached as adorner.
        /// </param>
        /// <param name="attributeRanges">
        /// Attribute ranges
        /// </param>
        internal CompositionAdorner(ITextView textView, ArrayList attributeRanges)
            : base(textView.RenderScope)
        {
            Debug.Assert(textView != null && textView.RenderScope != null);
 
            // TextView to which this CompositionAdorner is attached as adorner and it will
            // als be used for GetRectangleFromTextPosition/GetLineRange
            _textView = textView;
 
            // Create ArrayList for the composition attribute ranges and composition lines
            _attributeRanges = attributeRanges;
        }
 
        #endregion Constructors
 
        //------------------------------------------------------
        //
        //  Public Methods
        //
        //------------------------------------------------------
 
        #region Public Methods
 
        /// <summary>
        /// Add a transform so that the composition adorner gets positioned at the correct spot within the text being edited
        /// </summary>
        /// <param name="transform">
        /// The transform applied to the object the adorner adorns
        /// </param>
        /// <returns>
        /// Transform to apply to the adorner
        /// </returns>
        public override GeneralTransform GetDesiredTransform(GeneralTransform transform)
        {            
            TranslateTransform translation;
            GeneralTransformGroup group = new GeneralTransformGroup();
 
            // Get the matrix transform out, skip all non affine transforms
            Transform t = transform.AffineTransform;
            if (t == null)
            {                
                t = Transform.Identity;                
            }
 
            // Translate the adorner to (0, 0) point
            translation = new TranslateTransform(-(t.Value.OffsetX), -(t.Value.OffsetY));
 
            group.Children.Add(translation);
 
            if (transform != null)
            {
                group.Children.Add(transform);
            }
 
            return group;
        }
 
        #endregion Public Methods
 
        //------------------------------------------------------
        //
        //  Protected Methods
        //
        //------------------------------------------------------
 
        #region Protected Methods
 
        /// <summary>
        /// Render override to render the composition adorner here.
        /// </summary>
        protected override void OnRender(DrawingContext drawingContext)
        {
            // Get the matrix from AdornedElement to the visual parent to get the transformed
            // start/end point
            Visual parent2d = VisualTreeHelper.GetParent(this.AdornedElement) as Visual;
            if (parent2d == null)
            {
                return;
            }
 
            GeneralTransform transform = AdornedElement.TransformToAncestor(parent2d);
            if (transform == null)
            {
                return;
            }
 
            // Please note that we do the highlight adornment for the CONVERTED text only 
            // for Simplified Chinese IMEs. Doing this uniformly across all IMEs wasnt possible 
            // because it was noted that some of them (viz. Japanese) werent being consistent 
            // about this attribute.
 
            bool isChinesePinyin = chinesePinyin.Equals(InputLanguageManager.Current.CurrentInputLanguage.IetfLanguageTag);
 
            // Render the each of the composition string attribute from the attribute ranges.
            for (int i = 0; i < _attributeRanges.Count; i++)
            {
                DoubleCollection dashArray;
 
                // Get the composition attribute range from the attribute range lists
                AttributeRange attributeRange = (AttributeRange)_attributeRanges[i];
 
                // Skip the rendering composition lines if the composition line doesn't exist.
                if (attributeRange.CompositionLines.Count == 0)
                {
                    continue;
                }
 
                // Set the line bold and squiggle
                bool lineBold = attributeRange.TextServicesDisplayAttribute.IsBoldLine ? true : false;
                bool squiggle = false;
                bool hasVirtualSelection = (attributeRange.TextServicesDisplayAttribute.AttrInfo & UnsafeNativeMethods.TF_DA_ATTR_INFO.TF_ATTR_TARGET_CONVERTED) != 0;
 
                Brush selectionBrush = null;
                double selectionOpacity = -1;
                Pen selectionPen = null;
 
                if (isChinesePinyin && hasVirtualSelection)
                {
                    DependencyObject owner = _textView.TextContainer.Parent;
                    selectionBrush = (Brush)owner.GetValue(TextBoxBase.SelectionBrushProperty);
                    selectionOpacity = (double)owner.GetValue(TextBoxBase.SelectionOpacityProperty);
                }
 
                // Set the line height and cluse gap value that base on the ratio of text height
                double height = attributeRange.Height;
                double lineHeight = height * (lineBold ? BoldLineHeightRatio : NormalLineHeightRatio);
                double clauseGap = height * ClauseGapRatio;
 
                // Create Pen for drawing the composition lines with the specified line color
                Pen pen = new Pen(new SolidColorBrush(Colors.Black), lineHeight);
 
                // Set the pen style that based on IME's composition line style
                switch (attributeRange.TextServicesDisplayAttribute.LineStyle)
                {
                    case UnsafeNativeMethods.TF_DA_LINESTYLE.TF_LS_DOT:
                        // Add the dot length and specify the start/end line cap as the round
                        dashArray = new DoubleCollection();
                        dashArray.Add(DotLength);
                        dashArray.Add(DotLength);
 
                        pen.DashStyle = new DashStyle(dashArray, 0);
                        pen.DashCap = System.Windows.Media.PenLineCap.Round;
                        pen.StartLineCap = System.Windows.Media.PenLineCap.Round;
                        pen.EndLineCap = System.Windows.Media.PenLineCap.Round;
 
                        // Update the line height for the dot line. Dot line will be more thickness than
                        // other line to show it clearly.
                        lineHeight = height * (lineBold ? BoldDotLineHeightRatio : NormalDotLineHeightRatio);
 
                        break;
 
                    case UnsafeNativeMethods.TF_DA_LINESTYLE.TF_LS_DASH:
                        double dashLength = height * (lineBold ? BoldDashRatio : NormalDashRatio);
                        double dashGapLength = height * (lineBold ? BoldDashGapRatio : NormalDashGapRatio);
 
                        // Add the dash and dash gap legth
                        dashArray = new DoubleCollection();
                        dashArray.Add(dashLength);
                        dashArray.Add(dashGapLength);
 
                        pen.DashStyle = new DashStyle(dashArray, 0);
                        pen.DashCap = System.Windows.Media.PenLineCap.Round;
                        pen.StartLineCap = System.Windows.Media.PenLineCap.Round;
                        pen.EndLineCap = System.Windows.Media.PenLineCap.Round;
 
                        break;
 
                    case UnsafeNativeMethods.TF_DA_LINESTYLE.TF_LS_SOLID:
                        pen.StartLineCap = System.Windows.Media.PenLineCap.Round;
                        pen.EndLineCap = System.Windows.Media.PenLineCap.Round;
 
                        break;
 
                    case UnsafeNativeMethods.TF_DA_LINESTYLE.TF_LS_SQUIGGLE:
                        squiggle = true;
                        break;
                }
 
                double halfLineHeight = lineHeight / 2;
 
                // Draw the each of the composition line
                for (int j = 0; j < attributeRange.CompositionLines.Count; j++)
                {
                    CompositionLine compositionLine = (CompositionLine)attributeRange.CompositionLines[j];
 
                    // Get the start/end point for composition adorner.
                    // Currently Text doesn't aware of the spaceroom for the drawing of the composition 
                    // adorner(like as normal/bold dot/line/squggle), so we should draw the composition adorners
                    // to the closest area of the bottom text.
                    Point startPoint = new Point(compositionLine.StartPoint.X + clauseGap, compositionLine.StartPoint.Y - halfLineHeight);
                    Point endPoint = new Point(compositionLine.EndPoint.X - clauseGap, compositionLine.EndPoint.Y - halfLineHeight);
                    
                    // Apply composition line color which is actually the foreground of text as well
                    pen.Brush = new SolidColorBrush(compositionLine.LineColor);
 
                    // Apply matrix to start/end point
                    // REVIEW: if the points can't be transformed, should we not draw anything?
                    transform.TryTransform(startPoint, out startPoint);
                    transform.TryTransform(endPoint, out endPoint);
 
                    if (isChinesePinyin && hasVirtualSelection)
                    {
                        Rect rect = Rect.Union(compositionLine.StartRect, compositionLine.EndRect);
                        rect = transform.TransformBounds(rect);
                        
                        drawingContext.PushOpacity(selectionOpacity);
                    
                        drawingContext.DrawRectangle(selectionBrush, selectionPen, rect);
                    
                        drawingContext.Pop();
                    }
                    
                    if (squiggle)
                    {
                        // Draw the squiggle line with using of the PathFigure and DrawGemetry.
                        // We may revisit this logic to render the smooth squiggle line.
                        Point pathPoint = new Point(startPoint.X, startPoint.Y - halfLineHeight);
 
                        double squiggleGap = halfLineHeight;
 
                        PathFigure pathFigure = new PathFigure();
                        pathFigure.StartPoint = pathPoint;
 
                        int indexPoint = 0;
 
                        while (indexPoint < ((endPoint.X - startPoint.X) / (squiggleGap)))
                        {
                            if (indexPoint % 4 == 0 || indexPoint % 4 == 3)
                            {
                                pathPoint = new Point(pathPoint.X + squiggleGap, pathPoint.Y + halfLineHeight);
                                pathFigure.Segments.Add(new LineSegment(pathPoint, true));
                            }
                            else if (indexPoint % 4 == 1 || indexPoint % 4 == 2)
                            {
                                pathPoint = new Point(pathPoint.X + squiggleGap, pathPoint.Y - halfLineHeight);
                                pathFigure.Segments.Add(new LineSegment(pathPoint, true));
                            }
 
                            indexPoint++;
                        }
 
                        PathGeometry pathGeometry = new PathGeometry();
                        pathGeometry.Figures.Add(pathFigure);
 
                        // Draw the composition line with the squiggle
                        drawingContext.DrawGeometry(null, pen, pathGeometry);
                    }
                    else
                    {
                        drawingContext.DrawLine(pen, startPoint, endPoint);
                    }
                }
            }
        }
 
        #endregion Protected Events
 
        //------------------------------------------------------
        //
        //  Internal Methods
        //
        //------------------------------------------------------
 
        #region Internal Methods
 
        /// <summary>
        /// Add the composition attribute range that will be rendered.
        /// Used in TextServicesDisplayAttributePropertyRanges (for composition display attribute)
        /// </summary>
        internal void AddAttributeRange(ITextPointer start, ITextPointer end, TextServicesDisplayAttribute textServiceDisplayAttribute)
        {
            // Set the range start/end point's logical direction
            ITextPointer rangeStart = start.CreatePointer(LogicalDirection.Forward);
            ITextPointer rangeEnd = end.CreatePointer(LogicalDirection.Backward);
 
            // Add the composition attribute range
            _attributeRanges.Add(new AttributeRange(_textView, rangeStart, rangeEnd, textServiceDisplayAttribute));
        }
 
        /// <summary>
        /// Invalidates the CompositionAdorner render.
        /// Used in TextServicesDisplayAttributePropertyRanges (for composition display attribute)
        /// </summary>
        internal void InvalidateAdorner()
        {
            for (int i = 0; i < _attributeRanges.Count; i++)
            {
                // Get the composition attribute range from the attribute range lists
                AttributeRange attributeRange = (AttributeRange)_attributeRanges[i];
 
                // Add the composition lines for rendering the composition lines
                attributeRange.AddCompositionLines();
            }
 
            // Invalidate the CompositionAdorner to update the rendering.
            AdornerLayer adornerLayer = VisualTreeHelper.GetParent(this) as AdornerLayer;
            if (adornerLayer != null)
            {
                adornerLayer.Update(AdornedElement);
                adornerLayer.InvalidateArrange();
            }
        }
 
        /// <summary>
        /// Add CompositionAdorner to the scoping AdornerLayer.
        /// </summary>
        internal void Initialize(ITextView textView)
        {
            Debug.Assert(_adornerLayer == null, "Attempt to overwrite existing AdornerLayer!");
 
            _adornerLayer = AdornerLayer.GetAdornerLayer(textView.RenderScope);
 
            if (_adornerLayer != null)
            {
                // Add the CompositionAdorner to the scoping of AdornerLayer
                _adornerLayer.Add(this);
            }
        }
 
        /// <summary>
        /// Remove this CompositionAdorner from its AdornerLayer.
        /// </summary>
        internal void Uninitialize()
        {
            if (_adornerLayer != null)
            {
                // Remove CompositionAdorner form the socping of AdornerLayer
                _adornerLayer.Remove(this);
                _adornerLayer = null;
            }
        }
 
        #endregion Internal methods
 
        //------------------------------------------------------
        //
        //  Private Fields
        //
        //------------------------------------------------------
 
        #region Private Fields
 
        // AdornerLayer holding this CompositionAdorner visual
        private AdornerLayer _adornerLayer;
 
        // TextView to which this CompositionAdorner is attached as adorner
        private ITextView _textView;
 
        // ArrayList for the composition attribute ranges
        private readonly ArrayList _attributeRanges;
 
        // Composition line's dot length
        private const double DotLength = 1.2;
 
        // Ratio of the composition line
        private const double NormalLineHeightRatio = 0.06;
 
        // Ratio of the bold composition line
        private const double BoldLineHeightRatio = 0.08;
 
        // Ratio of the composition line of dot
        private const double NormalDotLineHeightRatio = 0.08;
 
        // Ratio of the bold composition line of dot
        private const double BoldDotLineHeightRatio = 0.10;
 
        // Ratio of the composition line's dash 
        private const double NormalDashRatio = 0.27;
 
        // Ratio of the bold composition line's dash 
        private const double BoldDashRatio = 0.39;
 
        // Ratio of the composition line's clause gap 
        private const double ClauseGapRatio = 0.09;
 
        // Ratio of the composition line's dash gap 
        private const double NormalDashGapRatio = 0.04;
 
        // Ratio of the bold composition line's dash gap 
        private const double BoldDashGapRatio = 0.06;
 
        // The culture name for Chinese Pinyin
        private const string chinesePinyin = "zh-CN";
 
        #endregion Private Fields
 
        //------------------------------------------------------
        //
        //  Private Class
        //
        //------------------------------------------------------
 
        private class AttributeRange
        {
            //------------------------------------------------------
            //
            //  Constructors
            //
            //------------------------------------------------------
 
            #region Constructors
 
            internal AttributeRange(ITextView textView, ITextPointer start, ITextPointer end, TextServicesDisplayAttribute textServicesDisplayAttribute)
            {
                _textView = textView;
                _startOffset = start.Offset;
                _endOffset = end.Offset;
 
                _textServicesDisplayAttribute = textServicesDisplayAttribute;
 
                _compositionLines = new ArrayList(1);
            }
 
            #endregion Constructors
 
            //------------------------------------------------------
            //
            //  Internal Methods
            //
            //------------------------------------------------------
 
            #region Internal Methods
 
            // Add the composition lines for rendering the composition lines.
            internal void AddCompositionLines()
            {
                // Erase any current lines.
                _compositionLines.Clear();
 
                ITextPointer start = _textView.TextContainer.Start.CreatePointer(_startOffset, LogicalDirection.Forward);
                ITextPointer end = _textView.TextContainer.Start.CreatePointer(_endOffset, LogicalDirection.Backward);
 
                while (start.CompareTo(end) < 0 && start.GetPointerContext(LogicalDirection.Forward) != TextPointerContext.Text)
                {
                    start.MoveToNextContextPosition(LogicalDirection.Forward);
                }
 
                Invariant.Assert(start.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text);
 
                if (end.HasValidLayout)
                {
                    // Get the rectangle for start/end position
                    _startRect = _textView.GetRectangleFromTextPosition(start);
                    _endRect = _textView.GetRectangleFromTextPosition(end);
 
                    // Check whether the composition line is single or multiple lines
                    if (_startRect.Top != _endRect.Top)
                    {
                        // Add the composition lines to be rendered for the composition string
                        AddMultipleCompositionLines(start, end);
                    }
                    else
                    {
                        // Set the start/end pointer to draw the line
                        Color lineColor = _textServicesDisplayAttribute.GetLineColor(start);
                        // Add the composition line to be rendered
                        _compositionLines.Add(new CompositionLine(_startRect, _endRect, lineColor));
                    }
                }
            }
 
            #endregion Internal Methods
 
            //------------------------------------------------------
            //
            //  Internal Properties
            //
            //------------------------------------------------------
 
            #region Internal Properties
 
            /// <summary>
            /// Height of the composition attribute range.
            /// </summary>
            internal double Height
            {
                get
                {
                    return _startRect.Bottom - _startRect.Top;
                }
            }
 
            /// <summary>
            /// CompositionLines of the composition attribute range.
            /// </summary>
            internal ArrayList CompositionLines
            {
                get
                {
                    return _compositionLines;
                }
            }
 
            /// <summary>
            /// Composition attribute information.
            /// </summary>
            internal TextServicesDisplayAttribute TextServicesDisplayAttribute
            {
                get
                {
                    return _textServicesDisplayAttribute;
                }
            }
 
            #endregion Internal Properties
 
            //------------------------------------------------------
            //
            //  Private Methods
            //
            //------------------------------------------------------
 
            #region Private Methods
 
            private void AddMultipleCompositionLines(ITextPointer start, ITextPointer end)
            {
                // Initalize the start/end line pointer
                ITextPointer startLinePointer = start;
                ITextPointer endLinePointer = startLinePointer;
 
                // Get all composition lines that includes the start/end pointer
                while (endLinePointer.CompareTo(end) < 0)
                {
                    TextSegment textSegment = _textView.GetLineRange(endLinePointer);
 
                    if (textSegment.IsNull)
                    {
                        // endLinePointer is not within the TextView's definition of a line.
                        // Skip ahead to text on the next iteration.
                        startLinePointer = endLinePointer;
                    }
                    else
                    {
                        Debug.Assert(start.CompareTo(startLinePointer) <= 0, "The start pointer is positioned after the composition start line pointer!");
 
                        if (startLinePointer.CompareTo(textSegment.Start) < 0)
                        {
                            // Update the start line pointer
                            startLinePointer = textSegment.Start;
                        }
 
                        if (endLinePointer.CompareTo(textSegment.End) < 0)
                        {
                            if (end.CompareTo(textSegment.End) < 0)
                            {
                                // Update the end line pointer
                                endLinePointer = end.CreatePointer();
                            }
                            else
                            {
                                // Update the end line pointer
                                endLinePointer = textSegment.End.CreatePointer(LogicalDirection.Backward);
                            }
                        }
                        else
                        {
                            Debug.Assert(endLinePointer.CompareTo(textSegment.End) == 0, "The end line pointer is positioned after the composition text range end pointer!");
                        }
 
                        // Get the rectangle for start/end position
                        Rect startRect = _textView.GetRectangleFromTextPosition(startLinePointer);
                        Rect endRect = _textView.GetRectangleFromTextPosition(endLinePointer);
 
                        // Add the composition line to be rendered
                        _compositionLines.Add(new CompositionLine(startRect, endRect, _textServicesDisplayAttribute.GetLineColor(startLinePointer)));
 
                        startLinePointer = textSegment.End.CreatePointer(LogicalDirection.Forward);
                    }
 
                    // Move the start pointer to the next text line. startLinePointer must be a pointer to start 
                    // text.
                    while ((startLinePointer.GetPointerContext(LogicalDirection.Forward) != TextPointerContext.None) &&
                           (startLinePointer.GetPointerContext(LogicalDirection.Forward) != TextPointerContext.Text))
                    {
                        startLinePointer.MoveToNextContextPosition(LogicalDirection.Forward);
                    }
                    endLinePointer = startLinePointer;
                }
            }
 
            #endregion Private methods
 
            //------------------------------------------------------
            //
            //  Private Fields
            //
            //------------------------------------------------------
 
            #region Private Fields
 
            // TextView to which this CompositionAdorner is attached as adorner
            private ITextView _textView;
 
            // Start rect of the composition attribute range
            private Rect _startRect;
 
            // End rect of the composition attribute range
            private Rect _endRect;
 
            // Start position offset of the composition attribute range
            private readonly int _startOffset;
 
            // End position offset of the composition attribute range
            private readonly int _endOffset;
 
            // Composition display attribute that is specified from IME
            private readonly TextServicesDisplayAttribute _textServicesDisplayAttribute;
 
            // ArrayList for the composition lines
            private readonly ArrayList _compositionLines;
 
            #endregion Private Fields
        }
 
        private class CompositionLine
        {
            //------------------------------------------------------
            //
            //  Constructors
            //
            //------------------------------------------------------
 
            #region Constructors
 
            internal CompositionLine(Rect startRect, Rect endRect, Color lineColor)
            {
                _startRect = startRect;
                _endRect = endRect;
                _color = lineColor;
            }
 
            #endregion Constructors
 
            //------------------------------------------------------
            //
            //  Internal Properties
            //
            //------------------------------------------------------
 
            #region Internal Properties
 
            /// <summary>
            /// Start point of the composition line draw
            /// </summary>
            internal Point StartPoint
            {
                get
                {
                    return _startRect.BottomLeft;
                }
            }
 
            /// <summary>
            /// End point of the composition line draw
            /// </summary>
            internal Point EndPoint
            {
                get
                {
                    return _endRect.BottomRight;
                }
            }
 
            /// <summary>
            /// Start rect of the composition line draw
            /// </summary>
            internal Rect StartRect
            {
                get 
                { 
                    return _startRect; 
                }
            }
            
            /// <summary>
            /// End rect of the composition line draw
            /// </summary>
            internal Rect EndRect
            {
                get 
                { 
                    return _endRect; 
                }
            }
            
            /// <summary>
            /// Color of the composition line draw
            /// </summary>
            internal Color LineColor
            {
                get
                {
                    return _color;
                }
            }
 
            #endregion Internal Properties
 
            //------------------------------------------------------
            //
            //  Private Fields
            //
            //------------------------------------------------------
 
            #region Private Fields
 
            // Start point of the composition line draw
            private Rect _startRect;
 
            // End point of the composition line draw
            private Rect _endRect;
 
            // Color of the composition line draw
            private Color _color;
 
            #endregion Private Fields
        }
    }
}