File: System\Windows\Documents\TextContainerChangedEventArgs.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: The arguments sent when a TextChangedEvent is fired in a TextContainer.
//
 
using System;
using System.Windows;
using System.Windows.Controls;
using System.Collections.Generic;
 
namespace System.Windows.Documents
{
    /// <summary>
    ///  The TextContainerChangedEventArgs defines the event arguments sent when a 
    ///  TextContainer is changed.
    /// </summary>
    internal class TextContainerChangedEventArgs : EventArgs
    {
        //------------------------------------------------------
        //
        //  Constructors
        //
        //------------------------------------------------------
 
        #region Constructors
 
        internal TextContainerChangedEventArgs()
        {
            _changes = new SortedList<int, TextChange>();
        }
 
        #endregion Constructors
 
        //------------------------------------------------------
        //
        //  Internal Methods
        //
        //------------------------------------------------------
 
        #region Internal Methods
 
        // Called by TextElement when a local property value changes.
        // Sets the HasLocalPropertyValueChange property true.
        internal void SetLocalPropertyValueChanged()
        {
            _hasLocalPropertyValueChange = true;
        }
 
        // Adds a new change to the Delta list.
        internal void AddChange(PrecursorTextChangeType textChange, int offset, int length, bool collectTextChanges)
        {
            if (textChange == PrecursorTextChangeType.ContentAdded ||
                textChange == PrecursorTextChangeType.ElementAdded ||
                textChange == PrecursorTextChangeType.ContentRemoved ||
                textChange == PrecursorTextChangeType.ElementExtracted)
            {
                _hasContentAddedOrRemoved = true;
            }
 
            if (!collectTextChanges)
            {
                return;
            }
 
            // We only want to add or remove the edges for empty elements (and ElementAdded /
            // ElementRemoved are only used with empty elements).  Note that since we're making
            // two changes instead of one the order matters.  We have to treat the two cases differently.
            if (textChange == PrecursorTextChangeType.ElementAdded)
            {
                AddChangeToList(textChange, offset, 1);
                AddChangeToList(textChange, offset + length - 1, 1);
            }
            else if (textChange == PrecursorTextChangeType.ElementExtracted)
            {
                AddChangeToList(textChange, offset + length - 1, 1);
                AddChangeToList(textChange, offset, 1);
            }
            else if (textChange == PrecursorTextChangeType.PropertyModified)
            {
                // ignore property changes
                return;
            }
            else
            {
                AddChangeToList(textChange, offset, length);
            }
        }
 
#if PROPERTY_CHANGES
        // We don't merge property changes with each other when they're added, so that when an element is removed
        // we can remove the property changes associated with it.  This method merges all of them at once.
        internal void MergePropertyChanges()
        {
            TextChange leftChange = null;
            for (int index = 0; index < Changes.Count; index++)
            {
                TextChange curChange = Changes.Values[index];
                if (leftChange == null || leftChange.Offset + leftChange.PropertyCount < curChange.Offset)
                {
                    leftChange = curChange;
                }
                else if (leftChange.Offset + leftChange.PropertyCount >= curChange.Offset)
                {
                    if (MergePropertyChange(leftChange, curChange))
                    {
                        // Changes merged.  If the right-hand change is now empty, remove it.
                        if (curChange.RemovedLength == 0 && curChange.AddedLength == 0)
                        {
                            Changes.RemoveAt(index--); // decrement index since we're removing an element, otherwise we'll skip one
                        }
                    }
                }
            }
        }
#endif
 
        #endregion Internal Methods
 
        //------------------------------------------------------
        //
        //  Internal Properties
        //
        //------------------------------------------------------
 
        #region Internal Properties
 
        /// <summary>
        /// Returns true if at least one of the TextChangeSegments in this TextChangeCollection
        /// is not of type TextChange.LayoutAffected or TextChange.RenderAffect.  This means that
        /// content was added or removed -- rather than content only affected by DependencyProperty
        /// values changes on scoping elements.
        /// </summary>
        /// <value></value>
        internal bool HasContentAddedOrRemoved
        {
            get
            {
                return _hasContentAddedOrRemoved;
            }
        }
 
        /// <summary>
        /// Returns true if the collection contains one or more entries that
        /// result from a local property value changing on a TextElement.
        /// </summary>
        /// <remarks>
        /// Note "local property value" does NOT include property changes
        /// that arrive via inheritance or styles.
        /// </remarks>
        internal bool HasLocalPropertyValueChange
        {
            get
            {
                return _hasLocalPropertyValueChange;
            }
        }
 
        /// <summary>
        /// List of TextChanges representing the aggregate changes made to the container.
        /// </summary>
        internal SortedList<int, TextChange> Changes
        {
            get
            {
                return _changes;
            }
        }
 
        #endregion Internal Properties
 
        #region Private Methods
 
        //------------------------------------------------------
        //
        //  Private Methods
        //
        //------------------------------------------------------
 
        //
        // First we discover where the new change goes in the sorted list.  If there's already
        // a change there, merge the new one with the old one.
        //
        // Next, if the new change is an insertion or deletion, see if the offset of the change
        // is within a range covered by an insertion at a lower offset.  If so, merge the change at 
        // the higher offset into the previous change and delete the change at the higher offset.
        //
        // Next, if the change is a deletion, see if we delete enough characters to run into a
        // change at a higher offset.  If so, merge the change at the higher offset into this newer
        // change and delete the older, higher-offset change.
        //
        // Finally, update all offsets higher than the current change to reflect the number of
        // characters inserted or deleted.
        //        
        private void AddChangeToList(PrecursorTextChangeType textChange, int offset, int length)
        {
            int offsetDelta = 0; // Value by which to adjust other offsets in list
            int curIndex;        // loop counter
            TextChange change = null; // initialize to satisfy compiler
            bool isDeletion = false;
#if PROPERTY_CHANGES
            bool shiftPropertyChanges = false;
#endif
 
            //
            // Is there already a change at this offset in our list?  If so, use it
            //
            int keyIndex = Changes.IndexOfKey(offset);
            if (keyIndex != -1)
            {
                change = Changes.Values[keyIndex];
            }
            else
            {
                change = new TextChange();
                change.Offset = offset;
                Changes.Add(offset, change);
                keyIndex = Changes.IndexOfKey(offset);
            }
 
            //
            // Merge values from new change into empty or pre-existing change
            //
            if (textChange == PrecursorTextChangeType.ContentAdded || textChange == PrecursorTextChangeType.ElementAdded)
            {
                change.AddedLength += length;
                offsetDelta = length;
#if PROPERTY_CHANGES
                if (change.PropertyCount > 0)
                {
                    shiftPropertyChanges = true;
                }
#endif
            }
            else if (textChange == PrecursorTextChangeType.ContentRemoved || textChange == PrecursorTextChangeType.ElementExtracted)
            {
                // Any deletions made after an addition just cancel out the earlier addition
                change.RemovedLength += Math.Max(0, length - change.AddedLength);
                change.AddedLength = Math.Max(0, change.AddedLength - length);
 
#if PROPERTY_CHANGES
                // If an element was extracted, any property change made on that element should be removed as well
                if (textChange == PrecursorTextChangeType.ElementExtracted)
                {
                    change.SetPropertyCount(0);
                }
                else
                {
                    change.SetPropertyCount(Math.Max(0, change.PropertyCount - length));
                }
#endif
 
                offsetDelta = -length;
                isDeletion = true;
            }
#if PROPERTY_CHANGES
            else
            {
                // Property change
                if (change.PropertyCount < length)
                {
                    change.SetPropertyCount(length);
                }
            }
#endif
 
            //
            // Done with simple case.  Now look for changes that intersect.
            // There are two possible (non-exclusive) merge conditions:
            //   1. A positionally preceding change spans this offset (this change is inserting
            //        into or deleting from the middle or right edge of previous inserted text)
            //   2. On deletion, the change spans one or more positionally later changes
            //
            if (keyIndex > 0 && textChange != PrecursorTextChangeType.PropertyModified)
            {
                curIndex = keyIndex - 1;
                // Since we don't merge property changes, there could be an arbitrary number of
                // them between the new change and an overlapping insertion or overlapping property
                // change.  In fact, there could be multiple property changes that overlap this
                // change.  We need to adjust ALL of them.  There can be only one overlapping
                // insertion, but there's no way to leverage that fact.
                TextChange mergedChange = null;
                while (curIndex >= 0)
                {
                    TextChange prevChange = Changes.Values[curIndex];
#if PROPERTY_CHANGES
                    if (prevChange.Offset + prevChange.AddedLength >= offset || prevChange.Offset + prevChange.PropertyCount >= offset)
#else
                    if (prevChange.Offset + prevChange.AddedLength >= offset)
#endif
                    {
                        if (MergeTextChangeLeft(prevChange, change, isDeletion, length))
                        {
                            mergedChange = prevChange;
                        }
                    }
                    curIndex--;
                }
                if (mergedChange != null)
                {
                    // the change got merged.  Use the merged change as the basis for righthand merging.
                    change = mergedChange;
                }
                // changes may have been deleted, so update the index of the change we're working with
                keyIndex = Changes.IndexOfKey(change.Offset);
            }
 
            curIndex = keyIndex + 1;
            if (isDeletion && curIndex < Changes.Count)
            {
                while (curIndex < Changes.Count && Changes.Values[curIndex].Offset <= offset + length)
                {
                    // offset and length must be passed because if we've also merged left, we haven't yet adjusted indices.
                    // Note that MergeTextChangeRight() always removes Changes.Values[curIndex] from the list, so
                    // we don't need to increment curIndex.
                    MergeTextChangeRight(Changes.Values[curIndex], change, offset, length);
                }
                // changes may have been deleted, so update the index of the change we're working with
                keyIndex = Changes.IndexOfKey(change.Offset);
            }
 
            // Update all changes to reflect new offsets.
            // If offsetDelta is positive, go right to left; otherwise, go left to right.
            // The order of the offsets will never change, so we can use an indexer safely.
            // To avoid nasty N-squared perf, create a new list instead of moving things within
            // the old one.
            // Change the implementation of Changes to a more efficient structure.
            if (offsetDelta != 0)
            {
                SortedList<int, TextChange> newChanges = new SortedList<int, TextChange>(Changes.Count);
                for (curIndex = 0; curIndex < Changes.Count; curIndex++)
                {
                    TextChange curChange = Changes.Values[curIndex];
                    if (curIndex > keyIndex)
                    {
                        curChange.Offset += offsetDelta;
                    }
                    newChanges.Add(curChange.Offset, curChange);
                }
                _changes = newChanges;
            }
            
            DeleteChangeIfEmpty(change);
 
#if PROPERTY_CHANGES
            // Finally, if the change was an insertion and there are property changes starting
            // at this location, the insertion is not part of the property changes.  Shift the
            // property changes forward by the length of the insertion.
            if (shiftPropertyChanges)
            {
                int propertyCount = change.PropertyCount;
                change.SetPropertyCount(0);
                AddChangeToList(PrecursorTextChangeType.PropertyModified, offset + offsetDelta, propertyCount);
            }
#endif
        }
 
        private void DeleteChangeIfEmpty(TextChange change)
        {
#if PROPERTY_CHANGES
            if (change.AddedLength == 0 && change.RemovedLength == 0 && change.PropertyCount == 0)
#else
            if (change.AddedLength == 0 && change.RemovedLength == 0)
#endif
            {
                Changes.Remove(change.Offset);
            }
        }
 
        // returns true if the change merged into an earlier insertion
        private bool MergeTextChangeLeft(TextChange oldChange, TextChange newChange, bool isDeletion, int length)
        {
            // newChange is inserting or deleting text inside oldChange.
            int overlap;
 
#if PROPERTY_CHANGES
            // Check for a property change in the old change that overlaps the new change
            if (oldChange.Offset + oldChange.PropertyCount >= newChange.Offset)
            {
                if (isDeletion)
                {
                    overlap = oldChange.PropertyCount - (newChange.Offset - oldChange.Offset);
                    oldChange.SetPropertyCount(oldChange.PropertyCount - Math.Min(overlap, length));
                    DeleteChangeIfEmpty(oldChange);
                }
                else
                {
                    oldChange.SetPropertyCount(oldChange.PropertyCount + length);
                }
            }
#endif
            
            if (oldChange.Offset + oldChange.AddedLength >= newChange.Offset)
            {
                // If any text was deleted in the new change, adjust the added count of the
                // previous change accordingly.  The removed count of the new change must be
                // adjusted by the same amount.
                if (isDeletion)
                {
                    overlap = oldChange.AddedLength - (newChange.Offset - oldChange.Offset);
                    int cancelledCount = Math.Min(overlap, newChange.RemovedLength);
                    oldChange.AddedLength -= cancelledCount;
                    oldChange.RemovedLength += (length - cancelledCount);
                }
                else
                {
                    oldChange.AddedLength += length;
                }
#if PROPERTY_CHANGES
                if (newChange.PropertyCount == 0)
                {
                    // We've merged the data from the new change into an older one, so we can
                    // delete the change from the list.
                    Changes.Remove(newChange.Offset);
                }
                else
                {
                    // Can't delete the change, since there's pre-existing property change data, so
                    // just clear the data instead.
                    newChange.AddedLength = 0;
                    newChange.RemovedLength = 0;
                }
#else
                // We've merged the data from the new change into an older one, so we can
                // delete the change from the list.
                Changes.Remove(newChange.Offset);
#endif
                return true;
            }
            return false;
        }
 
        private void MergeTextChangeRight(TextChange oldChange, TextChange newChange, int offset, int length)
        {
            // If the old change is an addition, find the length of the overlap
            // and adjust the addition and new deletion counts accordingly
            int addedLengthOverlap = oldChange.AddedLength > 0 ? offset + length - oldChange.Offset : 0;
 
#if PROPERTY_CHANGES
            // Check for a property change in the new change that overlaps the old change
            int propertyOverlap = newChange.Offset + newChange.PropertyCount - oldChange.Offset;
            if (propertyOverlap > 0)
            {
                int delta = Math.Min(propertyOverlap, addedLengthOverlap);
                newChange.SetPropertyCount(newChange.PropertyCount - delta);
                // Don't need to adjust oldChange.PropertyCount, since oldChange is about to be removed
            }
#endif
            
            // adjust removed count
            if (addedLengthOverlap >= oldChange.AddedLength)
            {
                // old change is entirely within new one
                newChange.RemovedLength += (oldChange.RemovedLength - oldChange.AddedLength);
                Changes.Remove(oldChange.Offset);
            }
            else
            {
                newChange.RemovedLength += (oldChange.RemovedLength - addedLengthOverlap);
                newChange.AddedLength += (oldChange.AddedLength - addedLengthOverlap);
                Changes.Remove(oldChange.Offset);
            }
        }
 
#if PROPERTY_CHANGES
        private bool MergePropertyChange(TextChange leftChange, TextChange rightChange)
        {
            if (leftChange.Offset + leftChange.PropertyCount >= rightChange.Offset)
            {
                if (leftChange.Offset + leftChange.PropertyCount < rightChange.Offset + rightChange.PropertyCount)
                {
                    // right change is partially inside left, but not completely.
                    int overlap = leftChange.Offset + leftChange.PropertyCount - rightChange.Offset;
                    int delta = rightChange.PropertyCount - overlap;
                    leftChange.SetPropertyCount(leftChange.PropertyCount + delta);
                }
                rightChange.SetPropertyCount(0);
                return true;
            }
            return false;
        }
#endif
 
        #endregion Private Methods
        
        //------------------------------------------------------
        //
        //  Private Fields
        //
        //------------------------------------------------------
 
        #region Private Fields
 
        // true if at least one of the changes in this TextChangeCollection
        // is not of type TextChange.LayoutAffected or TextChange.RenderAffect.
        private bool _hasContentAddedOrRemoved;
 
        // True if the collection contains one or more entries that
        // result from a local property value changing on a TextElement.
        private bool _hasLocalPropertyValueChange;
 
        private SortedList<int, TextChange> _changes;
 
        #endregion Private Fields
    }
}