Click here to Skip to main content
15,891,184 members
Articles / Programming Languages / C#

Resolving Symbolic References in a CodeDOM (Part 7)

Rate me:
Please Sign up or sign in to vote.
4.75/5 (6 votes)
2 Dec 2012CDDL12 min read 19.4K   509   14  
Resolving symbolic references in a CodeDOM.
// The Nova Project by Ken Beckett.
// Copyright (C) 2007-2012 Inevitable Software, all rights reserved.
// Released under the Common Development and Distribution License, CDDL-1.0: http://opensource.org/licenses/cddl1.php

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Effects;
using System.Windows.Media.Imaging;
using System.Windows.Threading;

using Nova.CodeDOM;
using Attribute = Nova.CodeDOM.Attribute;

namespace Nova.UI
{
    /// <summary>
    /// Used to format code objects as WPF objects to be displayed in a UI.
    /// </summary>
    /// <remarks>
    /// This class keeps track of the borders and stackpanels as the code is rendered, including
    /// handling the indentation level as new "lines" of code are emitted.  It also supports
    /// "pending comments" that are written in block style if not the last thing on the line.  And,
    /// it keeps track of the current line number, and fires events when newlines are created.
    /// </remarks>
    public class CodeRenderer
    {
        #region /* STATICS */

        /// <summary>
        /// The default proportional font size.
        /// </summary>
        public static readonly double DefaultProportionalFontSize = SystemFonts.MessageFontSize;

        // Ensure that static init of UI objects in CodeObject is done on the main thread by referencing CodeObject here!
        protected static readonly Brush DUMMY_BRUSH = CodeObjectVM.DarkLavender;

        protected static FontFamily _codeFontFamily = new FontFamily("Courier New");
        protected static double _codeFontSize = DefaultCodeFontSize;
        protected static Typeface _codeFontTypeFace;
        protected static double _zoomLevel;     // The zoom multiple (1..N)
        protected static double _spaceWidth;    // Width of a space in pixels for the current CodeFontFamily
        protected static double _fontHeight;    // Height of the current CodeFontFamily
        protected static double _fontBaseline;  // Baseline of the current CodeFontFamily
        protected static bool _usingCourierNew;
        protected static Dictionary<string, double> _codeTextWidths = new Dictionary<string, double>(); 
        protected static double _tinyFontSize = DefaultTinyFontSize;
        protected static FontFamily _proportionalFontFamily = SystemFonts.MessageFontFamily;
        protected static double _proportionalFontSize = DefaultProportionalFontSize;
        protected static Typeface _proportionalFontTypeFace;

        /// <summary>
        /// Determines if borders should be displayed.
        /// </summary>
        public static bool ShowBorders = true;

        /// <summary>
        /// Determines if background colors should be displayed.
        /// </summary>
        public static bool ShowBackgroundColors = true;

        /// <summary>
        /// Determines if gradient shading should be used.
        /// </summary>
        public static bool UseShading = true;

        /// <summary>
        /// Determines if half-height blank lines should be used.
        /// </summary>
        public static bool HalfHeightBlankLines;

        /// <summary>
        /// Determines if borders should be used on all sub-expressions.
        /// </summary>
        public static bool MaximizeBorders;

        /// <summary>
        /// Determines if rendering should be virtualized using Canvases so that WPF objects exist only for visible items.
        /// </summary>
        public static bool VirtualizeRendering = true;

        protected static void RecalculateCodeFont()
        {
            // Clear the dictionary of code text widths and re-calculate some values
            _codeTextWidths.Clear();
            _zoomLevel = _codeFontSize / DefaultCodeFontSize;
            _codeFontTypeFace = new Typeface(_codeFontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
            FormattedText space = new FormattedText(" ", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, _codeFontTypeFace, _codeFontSize, Brushes.Black);
            _spaceWidth = space.WidthIncludingTrailingWhitespace;
            _fontHeight = space.Height;
            _fontBaseline = space.Baseline;

            // Set a flag if we're using the Courier New font
            _usingCourierNew = (_codeFontFamily.Source == "Courier New");
        }

        protected static void RecalculateProportionalFont()
        {
            _proportionalFontTypeFace = new Typeface(_proportionalFontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
        }

        /// <summary>
        /// Purge any cached data (such as widths of strings in the current font).
        /// </summary>
        public static void PurgeCachedData()
        {
            _codeTextWidths.Clear();
        }

        #endregion

        #region /* CONSTANTS */

        /// <summary>
        /// The minimum valid font size.
        /// </summary>
        public const double MinimumValidFontSize = 0.004;

        /// <summary>
        /// Default to 13px (10pt) for the code font.
        /// </summary>
        public const double DefaultCodeFontSize = 13;

        /// <summary>
        /// Default to 8px for the tiny braces font.
        /// </summary>
        public const double DefaultTinyFontSize = 8;

        #endregion

        #region /* FIELDS */

        public bool Measuring;
        public double StartY, EndY;
        public ContextMenuEventHandler ToolTipContextMenuOpening;
        public CommandBindingCollection CommandBindings;

        protected StackPanel _topMost;
        protected Border _currentBorder;
        protected StackPanel _currentBlock;
        protected Canvas _currentCanvas;
        protected WrapPanel _currentLine;
        protected TextBlock _currentLineWrapper;  // Used to wrap TextBlocks of different fonts to align their baselines
        protected bool _isNewLine = true;
        protected int _lineNumber;
        protected bool _isGenerated;
        protected Stack<BorderIndentState> _borderIndentStateStack = new Stack<BorderIndentState>();
        protected Stack<MeasureState> _measureStateStack = new Stack<MeasureState>();

        #endregion

        #region /* CONSTRUCTORS */

        static CodeRenderer()
        {
            RecalculateCodeFont();
            RecalculateProportionalFont();
            InitializeCommands();
        }

        /// <summary>
        /// Create a <see cref="CodeRenderer"/> that renders into the provided <see cref="StackPanel"/>, rendering the area
        /// defined by startY/endY.
        /// </summary>
        public CodeRenderer(StackPanel stackPanel, double startY, double endY, bool isGenerated)
        {
#if DEBUG_VIRTUALIZATION
            if (endY < double.MaxValue)
            {
                double viewHeight = endY - startY;
                double margin = viewHeight * 0.25;
                startY += margin;
                endY -= margin;
            }
#endif
            StartY = startY;
            EndY = endY;
            _topMost = _currentBlock = stackPanel;
            _isGenerated = isGenerated;
            _borderIndentStateStack.Push(new BorderIndentState(null));
            _measureStateStack.Push(new MeasureState(null, 0, 0, 0));
            NewLine();

            stackPanel.MouseMove += stackPanel_MouseMove;
            InitializeToolTipTimer();
        }

        /// <summary>
        /// Create a <see cref="CodeRenderer"/> that renders into the provided <see cref="StackPanel"/>, rendering the area
        /// defined by startY/endY.
        /// </summary>
        public CodeRenderer(StackPanel stackPanel)
            : this(stackPanel, 0, double.MaxValue, false)
        { }

        #endregion

        #region /* PROPERTIES */

        /// <summary>
        /// The current line number.
        /// </summary>
        public int LineNumber
        {
            get { return _lineNumber; }
        }

        /// <summary>
        /// True if a newline is required before any other text, such as if a compiler directive was just emitted
        /// (used to force a newline after an "infix" compiler directive).
        /// </summary>
        public bool NeedsNewLine { get; set; }

        /// <summary>
        /// True if the code being rendered is generated (such as a generated '.g.cs' file).  Code cleanup settings will be ignored.
        /// </summary>
        public bool IsGenerated
        {
            get { return _isGenerated; }
        }

        /// <summary>
        /// The font family used to render code.
        /// </summary>
        public static FontFamily CodeFontFamily
        {
            get { return _codeFontFamily; }
            set
            {
                _codeFontFamily = value;
                RecalculateCodeFont();
            }
        }

        /// <summary>
        /// The font size for code.
        /// </summary>
        public static double CodeFontSize
        {
            get { return _codeFontSize; }
            set
            {
                _codeFontSize = (value < MinimumValidFontSize ? MinimumValidFontSize : value);
                RecalculateCodeFont();

                // Automatically scale the proportional font size based upon the relative
                // change in the code font size from the default.
                ProportionalFontSize = DefaultProportionalFontSize - (DefaultCodeFontSize - _codeFontSize);
            }
        }

        /// <summary>
        /// The height of the code font.
        /// </summary>
        public static double CodeFontHeight
        {
            get { return _fontHeight; }
        }

        /// <summary>
        /// The font size used for tiny braces mode.
        /// </summary>
        public static double TinyFontSize
        {
            get { return _tinyFontSize; }
            set { _tinyFontSize = (value < MinimumValidFontSize ? MinimumValidFontSize : value); }
        }

        /// <summary>
        /// The font family used to render proportional text.
        /// </summary>
        public static FontFamily ProportionalFontFamily
        {
            get { return _proportionalFontFamily; }
            set
            {
                _proportionalFontFamily = value;
                RecalculateProportionalFont();
            }
        }

        /// <summary>
        /// The font size for proportional text.
        /// </summary>
        public static double ProportionalFontSize
        {
            get { return _proportionalFontSize; }
            set
            {
                _proportionalFontSize = (value < MinimumValidFontSize ? MinimumValidFontSize : value);
                RecalculateProportionalFont();
            }
        }

        /// <summary>
        /// The width of a space for the current CodeFontFamily.
        /// </summary>
        public static double SpaceWidth
        {
            get { return _spaceWidth; }
        }

        /// <summary>
        /// The height of the current CodeFontFamily.
        /// </summary>
        public static double FontHeight
        {
            get { return _fontHeight; }
        }

        /// <summary>
        /// Returns true if CodeFontFamily is Courier New, otherwise false.
        /// </summary>
        public static bool UsingCourierNew
        {
            get { return _usingCourierNew; }
        }

        #endregion

        #region /* METHODS */

        /// <summary>
        /// A callback action used to re-render everything.
        /// </summary>
        public static Action ReRenderAll;

        /// <summary>
        /// Render a new line.
        /// </summary>
        public void NewLine(bool noIndent)
        {
            ++_lineNumber;
            NeedsNewLine = false;

            if (Measuring)
            {
                // Update the Height & Width of the current VM if necessary
                MeasureState currentMeasureState = _measureStateStack.Peek();
                CodeObjectVM currentVM = currentMeasureState.CodeObjectVM;
                if (currentVM != null)
                {
                    currentVM.Height += currentMeasureState.LineHeight;
                    if (currentVM.Width < currentMeasureState.LineWidth)
                        currentVM.Width = currentMeasureState.LineWidth;
                    currentMeasureState.LineHeight = currentMeasureState.LineWidth
                        = currentMeasureState.MaxBaseline = currentMeasureState.MaxUnderBaseline = 0;
                }
            }
            else
            {
                _currentLine = null;
                _currentLineWrapper = null;
                _isNewLine = true;
            }

            // Indent if appropriate, making the pending relative indent amount into an absolute value
            if (!noIndent && _currentCanvas == null)  // Ignore indents if using a canvas
            {
                BorderIndentState borderIndentState = _borderIndentStateStack.Peek();
                if (borderIndentState.IndentOnNewLine > 0)
                {
                    if (borderIndentState.IndentStateStack == null)
                        borderIndentState.IndentStateStack = new Stack<IndentState>();
                    if (borderIndentState.IndentStateStack.Count > 0)
                        borderIndentState.PendingIndentState.IndentPosition += borderIndentState.IndentStateStack.Peek().IndentPosition;
                    borderIndentState.IndentStateStack.Push(borderIndentState.PendingIndentState);
                    borderIndentState.IndentOnNewLine = 0;
                }
                // Render any indentation
                if (borderIndentState.IndentStateStack != null && borderIndentState.IndentStateStack.Count > 0)
                    RenderText(new string(' ', borderIndentState.IndentStateStack.Peek().IndentPosition), CodeObjectVM.PUNC_BRUSH, borderIndentState.BaseCodeObjectVM);
            }
        }

        /// <summary>
        /// Render a new line.
        /// </summary>
        public void NewLine()
        {
            NewLine(false);
        }

        /// <summary>
        /// Render the specified number of new lines.
        /// </summary>
        public void NewLines(int count, CodeObjectVM tag)
        {
            if (count > 1 && _currentCanvas == null)  // Ignore blank lines if using a canvas
            {
                NewLine(true);  // No indent spaces until the final newline below
                RenderText(new string('\n', count - 2), CodeObjectVM.PUNC_BRUSH, _codeFontFamily,
                    HalfHeightBlankLines ? (_codeFontSize / 2) : _codeFontSize, 0, TextStyle.Normal, tag);
            }
            NewLine();
        }

        /// <summary>
        /// Render a list of <see cref="CodeObject"/>s.
        /// </summary>
        public void RenderList<T>(IEnumerable<T> enumerable, CodeObjectVM.RenderFlags flags, CodeObjectVM parent) where T : CodeObjectVM
        {
            if (enumerable == null)
                return;

            // Increase the indent level for any newlines that occur within the child list unless specifically told not to
            bool increaseIndent = !flags.HasFlag(CodeObjectVM.RenderFlags.NoIncreaseIndent);
            if (increaseIndent)
                IndentOnNewLineBegin(parent);

            // Render the items in the list
            bool isSingleLine = true;
            IEnumerator<T> enumerator = enumerable.GetEnumerator();
            bool isFirst = true;
            bool hasMore = enumerator.MoveNext();
            while (hasMore)
            {
                T codeObjectVM = enumerator.Current;
                hasMore = enumerator.MoveNext();
                if (codeObjectVM != null)
                {
                    if (codeObjectVM.IsFirstOnLine)
                        isSingleLine = false;

                    // Determine if the child object has a border
                    bool childHasBorder = ((flags.HasFlag(CodeObjectVM.RenderFlags.ForceBorder) || codeObjectVM.HasBorder()) && !flags.HasFlag(CodeObjectVM.RenderFlags.NoBorder));

                    // Render the code object, omitting any EOL comments (so they can be rendered later after
                    // any comma), and prefixing a space if it's not the first item.
                    codeObjectVM.Render(this, flags | CodeObjectVM.RenderFlags.NoEOLComments | (childHasBorder ? 0 : CodeObjectVM.RenderFlags.NoPostAnnotations)
                        | (isSingleLine ? (isFirst ? 0 : CodeObjectVM.RenderFlags.PrefixSpace) : CodeObjectVM.RenderFlags.PrefixSpace));
                    flags &= ~(CodeObjectVM.RenderFlags.SuppressNewLine | CodeObjectVM.RenderFlags.NoPreAnnotations);

                    if (hasMore)
                    {
                        // Render the trailing comma, with EOL comments before or after it, depending on whether or not it's the last thing on the line
                        CodeObjectVM nextObject = enumerator.Current;
                        bool isLastOnLine = (nextObject != null && nextObject.IsFirstOnLine);
                        if (!isLastOnLine)
                            codeObjectVM.RenderEOLComments(this, flags);
                        if (!flags.HasFlag(CodeObjectVM.RenderFlags.NoItemSeparators))
                            RenderText(CodeDOM.Expression.ParseTokenSeparator, CodeObjectVM.PUNC_BRUSH, parent);
                        if (isLastOnLine)
                            codeObjectVM.RenderEOLComments(this, flags);
                    }
                    else
                    {
                        if (flags.HasFlag(CodeObjectVM.RenderFlags.HasTerminator) && !flags.HasFlag(CodeObjectVM.RenderFlags.Description) && parent != null)
                            ((StatementVM)parent).RenderTerminator(this, flags);
                        codeObjectVM.RenderEOLComments(this, flags);
                    }
                    if (!childHasBorder)
                        codeObjectVM.RenderAnnotations(this, AnnotationFlags.IsPostfix, flags);
                }
                else if (hasMore)
                    RenderText(CodeDOM.Expression.ParseTokenSeparator, CodeObjectVM.PUNC_BRUSH, parent);
                isFirst = false;
            }

            // Reset the indent level
            if (increaseIndent)
                IndentOnNewLineEnd(parent);
        }

        public void RenderVisibleList<T>(IEnumerable<T> enumerable, CodeObjectVM.RenderFlags flags) where T : CodeObjectVM
        {
            if (enumerable != null)
            {
                foreach (T codeObjectVM in enumerable)
                {
                    if (codeObjectVM != null)
                        codeObjectVM.RenderVisible(this, flags);
                }
            }
        }

        /// <summary>
        /// The number of <see cref="TextBlock"/>s created.
        /// </summary>
        public int TextBlockCount;

        /// <summary>
        /// Render the specified text.
        /// </summary>
        public TextBlock RenderText(string text, Brush brush, FontFamily fontFamily, double fontSize, int verticalOffset, TextStyle style, CodeObjectVM tag)
        {
            if (NeedsNewLine)
                NewLine();

            FontWeight fontWeight = (style.HasFlag(TextStyle.Bold) ? FontWeights.Bold : FontWeights.Normal);
            FontStyle fontStyle = (style.HasFlag(TextStyle.Italic) ? FontStyles.Italic : FontStyles.Normal);

            TextBlock textBlock;
            if (Measuring)
            {
                // Update the Height & Width of the current VM
                MeasureState currentMeasureState = _measureStateStack.Peek();
                CodeObjectVM currentVM = currentMeasureState.CodeObjectVM;
                if (currentVM != null)
                {
                    // Determine the height and width of the text, caching common results.  We get good performance savings even
                    // if not caching comments and long string literals, and even if the cache isn't static (so only caches the
                    // current rendering).  However, memory usage for the string/double cache isn't too bad, and we get even
                    // better performance by just caching everything and having the cache itself be static (it gets cleared when
                    // the solution is unloaded, or if the code font is changed).
                    double width, height, baseline, underBaseline;
                    if (style.HasFlag(TextStyle.NoCache) || fontFamily != _codeFontFamily || fontSize != _codeFontSize || fontWeight != FontWeights.Normal || fontStyle != FontStyles.Normal)
                    {
                        // For comments or anything other than the code font, size, and normal style, calculate the height & width without caching
                        Typeface typeFace = (fontFamily == _proportionalFontFamily && fontWeight == FontWeights.Normal && fontStyle == FontStyles.Normal)
                            ? _proportionalFontTypeFace : new Typeface(fontFamily, fontStyle, fontWeight, FontStretches.Normal);
                        if (text.Length > 0 && text[0] == '\n')  // Workaround newline issue with FormattedText
                            text = "\n" + text;
                        FormattedText formattedText = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeFace, fontSize, brush);
                        width = formattedText.WidthIncludingTrailingWhitespace;
                        height = formattedText.Height;
                        if (height == 0)
                        {
                            // For empty strings or control chars, use the height of a space of the same Typeface and size
                            formattedText = new FormattedText(" ", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeFace, fontSize, brush);
                            height = formattedText.Height;
                        }
                        // Adjust height if vertical position is negative
                        if (verticalOffset < 0)
                            height += verticalOffset;
                        // Very ugly way to check if only one line, since FormattedText doesn't tell you
                        if (height < formattedText.Baseline * 2)
                        {
                            baseline = formattedText.Baseline;
                            underBaseline = height - baseline;
                        }
                        else
                            baseline = underBaseline = 0;  // Ignore baseline calcs for multi line
                    }
                    else
                    {
                        // If it's the code font and size and normal style, look for a cached width for a big performance boost
                        if (string.IsNullOrEmpty(text))
                        {
                            width = 0;
                            height = _fontHeight;
                            baseline = _fontBaseline;
                            underBaseline = _fontHeight - _fontBaseline;
                        }
                        else if (_codeTextWidths.TryGetValue(text, out width))
                        {
                            height = _fontHeight;
                            baseline = _fontBaseline;
                            underBaseline = _fontHeight - _fontBaseline;
                        }
                        else
                        {
                            // Calculate the height & width
                            if (text.Length > 0 && text[0] == '\n')  // Workaround newline issue with FormattedText
                                text = "\n" + text;
                            FormattedText formattedText = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, _codeFontTypeFace, _codeFontSize, brush);
                            width = formattedText.WidthIncludingTrailingWhitespace;
                            height = formattedText.Height;
                            // Correct issues with FormattedText - it gives an empty string zero height, and it doesn't add a line
                            // for a starting newline, only subsequent ones (TextBlock handles these correctly).
                            if (height == 0)
                                height = _fontHeight;
                            // Don't cache multi-line text so we don't have to cache the height (should only be comments, anyway)
                            if (height == _fontHeight)
                            {
                                _codeTextWidths.Add(text, width);
                                baseline = _fontBaseline;
                                underBaseline = _fontHeight - _fontBaseline;
                            }
                            else
                                baseline = underBaseline = 0;  // Ignore baseline calcs for multi line
                        }
                    }

                    // Handle baseline adjustments for different fonts on the same line
                    bool adjustHeight = false;
                    if (currentMeasureState.MaxBaseline < baseline)
                    {
                        currentMeasureState.MaxBaseline = baseline;
                        adjustHeight = true;
                    }
                    if (currentMeasureState.MaxUnderBaseline < underBaseline)
                    {
                        currentMeasureState.MaxUnderBaseline = underBaseline;
                        adjustHeight = true;
                    }
                    if (adjustHeight)
                    {
                        double lineHeight = currentMeasureState.MaxBaseline + currentMeasureState.MaxUnderBaseline;
                        if (height < lineHeight)
                            height = lineHeight;
                    }

                    if (currentMeasureState.LineHeight < height)
                        currentMeasureState.LineHeight = height;
                    currentMeasureState.LineWidth += width;
                }
                textBlock = null;
            }
            else
            {
                textBlock = new TextBlock
                                {
                                    Text = text,
                                    Foreground = brush,
                                    FontFamily = fontFamily,
                                    FontSize = fontSize,
                                    FontWeight = fontWeight,
                                    FontStyle = fontStyle,
                                    VerticalAlignment = VerticalAlignment.Center,
                                    SnapsToDevicePixels = true,
                                    Tag = tag
                                };
                if (style.HasFlag(TextStyle.Bold))
                    textBlock.FontWeight = FontWeights.Bold;
                if (style.HasFlag(TextStyle.Italic))
                    textBlock.FontStyle = FontStyles.Italic;
                if (tag != null)
                {
                    textBlock.MouseEnter += textBlock_MouseEnter;
                    textBlock.MouseLeave += textBlock_MouseLeave;
                    textBlock.MouseDown += textBlock_MouseDown;
                    if (tag.FrameworkElement == null)
                        tag.FrameworkElement = textBlock;
                }
                ++TextBlockCount;

                // Adjust vertical position if so requested (allows operators to be aligned properly)
                if (verticalOffset != 0)
                {
                    // The vertical offset is in pixels at the default 13 pixel size, so adjust it for other sizes,
                    // and then double it because we have vertical centering turned on.
                    double topMargin = (verticalOffset * (fontSize / 13));
                    textBlock.Margin = new Thickness(0, topMargin, 0, 0);
                    textBlock.Measure(new Size(double.MaxValue, double.MaxValue));
                    Size size = textBlock.DesiredSize;
                    // Set MaxHeight to clip it (doesn't include the Margin, but DesiredSize does, so subtract it twice)
                    textBlock.MaxHeight = size.Height - topMargin * 2;
                    size.Height -= topMargin;
                }

                if (_currentLine != null)
                    AddElementToCurrentLine(textBlock);
                else if (_currentBlock != null)
                    AddElementToCurrentBlock(textBlock);
                else if (_currentBorder != null)  // Ignore text put directly in a Canvas, such as spaces between statements on the same line
                    AddElementToCurrentBorder(textBlock);
                _isNewLine = false;
            }

            return textBlock;
        }

        /// <summary>
        /// Render the specified text.
        /// </summary>
        public TextBlock RenderText(string text, Brush brush, TextStyle style, CodeObjectVM tag)
        {
            if (style.HasFlag(TextStyle.Proportional))
                return RenderText(text, brush, _proportionalFontFamily, _proportionalFontSize, 0, style, tag);
            return RenderText(text, brush, _codeFontFamily, _codeFontSize, 0, style, tag);
        }

        /// <summary>
        /// Render the specified text.
        /// </summary>
        public TextBlock RenderText(string text, Brush brush, TextStyle style)
        {
            return RenderText(text, brush, style, null);
        }

        /// <summary>
        /// Render the specified text.
        /// </summary>
        public TextBlock RenderText(string text, Brush brush, FontFamily fontFamily, int verticalOffset, CodeObjectVM tag)
        {
            return RenderText(text, brush, fontFamily, _codeFontSize, verticalOffset, TextStyle.Normal, tag);
        }

        /// <summary>
        /// Render the specified text.
        /// </summary>
        public TextBlock RenderText(string text, Brush brush, FontFamily fontFamily, CodeObjectVM tag)
        {
            return RenderText(text, brush, fontFamily, _codeFontSize, 0, TextStyle.Normal, tag);
        }

        /// <summary>
        /// Render the specified text.
        /// </summary>
        public TextBlock RenderText(string text, Brush brush, CodeObjectVM tag)
        {
            return RenderText(text, brush, _codeFontFamily, _codeFontSize, 0, TextStyle.Normal, tag);
        }

        /// <summary>
        /// Render a name, hiding any 'Attribute' suffix if it's an attribute name.
        /// </summary>
        public TextBlock RenderName(string name, Brush brush, CodeObjectVM tag, CodeObjectVM.RenderFlags flags)
        {
            // Hide any "Attribute" suffix for attribute constructor names
            if (flags.HasFlag(CodeObjectVM.RenderFlags.Attribute) && name.EndsWith(Attribute.NameSuffix))
                name = name.Substring(0, name.Length - Attribute.NameSuffix.Length);
            return RenderText(name, brush, tag);
        }

        /// <summary>
        /// Render the specified name and value.
        /// </summary>
        public void RenderNameValue(string name, string value)
        {
            NewLine();
            RenderText(name + ": ", CodeObjectVM.NORMAL_BRUSH, TextStyle.Proportional | TextStyle.Bold);
            RenderText(value, CodeObjectVM.NORMAL_BRUSH, TextStyle.Proportional);
        }

        /// <summary>
        /// The font used to render a right-arrow.
        /// </summary>
        public static FontFamily RightArrowFont = new FontFamily("Wingdings 3");

        /// <summary>
        /// Render a right-arrow.
        /// </summary>
        public void RenderRightArrow(Brush brush, CodeObjectVM tag)
        {
            RenderText("\x92 ", brush, RightArrowFont, _codeFontSize, 0, TextStyle.Normal, tag);
        }

        /// <summary>
        /// Render the image in the specified file name.
        /// </summary>
        public void RenderImage(string fileName, int height, int width, CodeObjectVM tag)
        {
            if (Measuring)
            {
                // Adjust size for zoom level (based on font size)
                double actualHeight = height * _zoomLevel;
                double actualWidth = width * _zoomLevel;

                // Update the Height & Width of the current VM
                MeasureState currentMeasureState = _measureStateStack.Peek();
                CodeObjectVM currentVM = currentMeasureState.CodeObjectVM;
                if (currentVM != null)
                {
                    if (currentMeasureState.LineHeight < actualHeight)
                        currentMeasureState.LineHeight = actualHeight;
                    currentMeasureState.LineWidth += actualWidth;
                }
            }
            else
            {
                Image image = new Image
                    {
                        Source = new BitmapImage(new Uri(fileName, UriKind.Relative)),
                        Height = height,
                        Width = width,
                        Margin = new Thickness(2, 0, 4, 0),  // Hard-code left & right margins for now
                        Tag = tag
                    };
                image.MouseEnter += image_MouseEnter;
                image.MouseLeave += image_MouseLeave;

                if (_currentBlock == null)
                    CreateBlock(0);
                CreateLine(_currentBlock);
                AddElementToCurrentLine(image);
                _isNewLine = false;
            }
        }

        /// <summary>
        /// The number of <see cref="WrapPanel"/>s created.
        /// </summary>
        public int WrapPanelCount;

        /// <summary>
        /// Create a new 'line' of code.
        /// </summary>
        public WrapPanel CreateLine(FrameworkElement parent)
        {
            // Set the Tag of the WrapPanel to match its parent's Tag instead of the object that actually
            // creates it (this makes more sense considering the way that WrapPanels are used).
            WrapPanel wrapPanel = new WrapPanel { Tag = parent.Tag, VerticalAlignment = VerticalAlignment.Center };
            ++WrapPanelCount;

            _currentLine = wrapPanel;
            _currentLineWrapper = null;
            if (parent is Panel)  // Block
                ((Panel)parent).Children.Add(wrapPanel);
            else
                ((Border)parent).Child = wrapPanel;
            return wrapPanel;
        }

        /// <summary>
        /// The number of <see cref="StackPanel"/>s created.
        /// </summary>
        public int StackPanelCount;

        /// <summary>
        /// Create a new 'block' of code.
        /// </summary>
        public StackPanel CreateBlock(int indentSpaces)
        {
            if (Measuring) return null;

            StackPanel stackPanel = new StackPanel { HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Center };
            if (indentSpaces > 0)
                stackPanel.Margin = new Thickness(indentSpaces * SpaceWidth, 0, 0, 0);
            ++StackPanelCount;

            _isNewLine = true;  // Blocks are always on new lines
            if (_currentBlock != null)
            {
                _currentBlock.Children.Add(stackPanel);
                stackPanel.Tag = _currentBlock.Tag;
            }
            else //if (_currentBorder != null)
            {
                AddElementToCurrentBorder(stackPanel);
                stackPanel.Tag = _currentBorder.Tag;
            }
            _currentBlock = stackPanel;
            _currentLine = null;
            _currentLineWrapper = null;
            return stackPanel;
        }

        /// <summary>
        /// Create a <see cref="Canvas"/> for partial rendering of a <see cref="BlockVM"/>.
        /// </summary>
        public Canvas CreateCanvas(double height, double width)
        {
            // A Canvas should only be created as a new 'line' in an existing block, or part of an existing 'line' (after an indent)
            _currentCanvas = new Canvas { Height = height, Width = width };
            if (_currentLine != null)
                AddElementToCurrentLine(_currentCanvas);
            else if (_currentBlock != null)
                AddElementToCurrentBlock(_currentCanvas);
            else //if (_currentBorder != null)
                AddElementToCurrentBorder(_currentCanvas);
            _isNewLine = false;
            _currentBorder = null;
            _currentBlock = null;
            _currentLine = null;
            _currentLineWrapper = null;
            return _currentCanvas;
        }

        /// <summary>
        /// Set the current <see cref="Canvas"/> object (used for virtualized rendering).
        /// </summary>
        public void SetCurrentCanvas(Canvas canvas)
        {
            _currentCanvas = canvas;
        }

        /// <summary>
        /// The number of <see cref="Border"/>s created.
        /// </summary>
        public int BorderCount;

        /// <summary>
        /// Create a <see cref="Border"/>.
        /// </summary>
        public void CreateBorder(Brush borderBrush, Brush backgroundBrush, CodeObjectVM codeObjectVM, double leftIndent, int verticalPad, int horizontalPad)
        {
            // Determine border padding, accounting for zoom level (based on font size)
            bool usePadding = (ShowBackgroundColors || ShowBorders);
            double actualLeftPad, actualRightPad, actualVerticalPad;
            if (usePadding)
            {
                actualVerticalPad = verticalPad * _zoomLevel;
                actualLeftPad = (leftIndent > 0 ? (horizontalPad + leftIndent) : horizontalPad) * _zoomLevel;
                actualRightPad = horizontalPad * _zoomLevel;
            }
            else
            {
                actualVerticalPad = 0;
                actualLeftPad = (leftIndent > 0 ? (leftIndent * _zoomLevel) : 0);
                actualRightPad = 0;
            }

            if (Measuring)
            {
                // Clear the Height & Width, and push the new MeasureState on the stack
                codeObjectVM.Height = codeObjectVM.Width = 0;
                if (ShowBorders)
                {
                    actualLeftPad += _zoomLevel;
                    actualRightPad += _zoomLevel;
                    actualVerticalPad += _zoomLevel;
                }
                MeasureState measureState = new MeasureState(codeObjectVM, actualLeftPad, actualRightPad, actualVerticalPad);
                _measureStateStack.Push(measureState);
            }
            else
            {
                Border border = new Border
                    {
                        Tag = codeObjectVM,
                        HorizontalAlignment = HorizontalAlignment.Left,
                        VerticalAlignment = VerticalAlignment.Center,
                        SnapsToDevicePixels = true, // Needed to prevent anti-aliasing
                        CornerRadius = new CornerRadius(_zoomLevel * 4)
                    };
                border.MouseEnter += border_MouseEnter;
                border.MouseLeave += border_MouseLeave;
                border.MouseDown += border_MouseDown;
                ++BorderCount;

                border.Padding = new Thickness(actualLeftPad, actualVerticalPad, actualRightPad, actualVerticalPad);

                if (ShowBorders)
                {
                    // Set border thickness, accounting for zoom level (based on font size)
                    border.BorderThickness = new Thickness(_zoomLevel);

                    if (UseShading && (borderBrush != Brushes.Transparent))
                    {
                        Color borderColor = ((SolidColorBrush)borderBrush).Color;
                        border.BorderBrush = new LinearGradientBrush(
                            new GradientStopCollection(3)
                            {
                                new GradientStop(Colors.LightGray, 0),
                                //new GradientStop(normal, 0.4),
                                new GradientStop(borderColor, 1)
                            },
                            new Point(0.5, 0), new Point(0.5, 1));
                    }
                    else
                        border.BorderBrush = borderBrush;
                }

                Color normalColor = (backgroundBrush is SolidColorBrush ? ((SolidColorBrush)backgroundBrush).Color : Colors.Black);
                Color lightColor = Colors.White;

                if (ShowBackgroundColors)
                {
                    if (UseShading && (backgroundBrush != Brushes.Transparent) && backgroundBrush is SolidColorBrush)
                    {
                        //Color.FromRgb(AddColor(dark.R, 24), AddColor(dark.G, 24), AddColor(dark.B, 24));
                        border.Background = new LinearGradientBrush(
                            new GradientStopCollection(3)
                            {
                                new GradientStop(normalColor, 0),
                                new GradientStop(lightColor, 0.4),
                                new GradientStop(normalColor, 1)
                            },
                            new Point(0.5, 0), new Point(0.5, 1));
                    }
                    else
                        border.Background = backgroundBrush;
                }
                else if (ShowBorders)
                    border.Background = Brushes.White;

                if (codeObjectVM != null && codeObjectVM.FrameworkElement == null)
                    codeObjectVM.FrameworkElement = border;

                if (_currentLine != null)
                    AddElementToCurrentLine(border);
                else if (_currentCanvas != null)
                    AddElementToCurrentCanvas(border, (codeObjectVM != null ? codeObjectVM.Y : 0), (codeObjectVM != null ? codeObjectVM.X : 0));
                else if (_currentBlock != null)
                    AddElementToCurrentBlock(border);
                else //if (_currentBorder != null)
                    AddElementToCurrentBorder(border);

                _isNewLine = false;
                _currentBorder = border;
                _currentBlock = null;
                _currentCanvas = null;
                _currentLine = null;
                _currentLineWrapper = null;
            }

            _borderIndentStateStack.Push(new BorderIndentState(codeObjectVM));
        }

        /// <summary>
        /// Create a <see cref="Border"/>.
        /// </summary>
        public void CreateBorder(Brush borderBrush, Brush backgroundBrush, CodeObjectVM codeObjectVM)
        {
            CreateBorder(borderBrush, backgroundBrush, codeObjectVM, 0, 1, 1);
        }

        protected void AddElementToLineWrapper(UIElement element)
        {
            InlineUIContainer container = new InlineUIContainer(element);
            if (element is TextBlock)
            {
                // If the text has a vertical adjustment, divide it by 2 because the baseline alignment overrides the
                // vertical centering that required that the offset be doubled.
                TextBlock textBlock = (TextBlock)element;
                if (textBlock.Margin.Top != 0)
                {
                    Thickness margin = textBlock.Margin;
                    textBlock.Margin = new Thickness(margin.Left, margin.Top / 2, margin.Right, margin.Bottom);
                }
            }
            else
            {
                // Nested non-TextBlocks (such as Border) don't get aligned based on nested TextBlock baselines inside them,
                // so tell WPF to center them within the parent instead.  This is off by a pixel for some reason (one pixel
                // above and 3 below in the parent) - one possible solution might be to make sure that the nested Border has
                // all different fonts of the parent represented within it, perhaps by adding hidden (NUL) characters.
                container.BaselineAlignment = BaselineAlignment.Center;
            }
            _currentLineWrapper.Inlines.Add(container);
        }

        protected void AddElementToCurrentLine(UIElement element)
        {
            // If we already have a wrapper, just add the element
            if (_currentLineWrapper != null)
                AddElementToLineWrapper(element);
            else
            {
                int lastIndex = _currentLine.Children.Count - 1;
                if (lastIndex >= 0)
                {
                    // If the font just changed, create a TextBlock wrapper to force alignment on the baselines of the children
                    UIElement lastTextBlock = _currentLine.Children[lastIndex];
                    while (!(lastTextBlock is TextBlock) && lastIndex > 0)
                        lastTextBlock = _currentLine.Children[--lastIndex];
                    if (lastTextBlock is TextBlock && element is TextBlock && ((TextBlock)lastTextBlock).FontFamily != ((TextBlock)element).FontFamily)
                    {
                        _currentLineWrapper = new TextBlock();
                        UIElementCollection children = _currentLine.Children;
                        while (children.Count > 0)
                        {
                            UIElement child = children[0];
                            children.RemoveAt(0);
                            if (child is Border)
                            {
                                // WPF seems to have some nasty issue that prevents Borders from being displayed once they are
                                // copied to the new wrapper (new Borders added later work OK).  Unable to figure out why... doesn't
                                // see to be a problem with the Parent or visual tree, and invalidating things doesn't work.
                                // Finally came up with a workaround of creating a new Border object, and that works.  WPF SUX!
                                Border oldBorder = (Border)child;
                                UIElement childElement = oldBorder.Child;
                                oldBorder.Child = null;
                                Border newBorder = new Border
                                    {
                                        Tag = oldBorder.Tag,
                                        HorizontalAlignment = oldBorder.HorizontalAlignment,
                                        VerticalAlignment = oldBorder.VerticalAlignment,
                                        SnapsToDevicePixels = oldBorder.SnapsToDevicePixels,
                                        CornerRadius = oldBorder.CornerRadius,
                                        Padding = oldBorder.Padding,
                                        BorderThickness = oldBorder.BorderThickness,
                                        BorderBrush = oldBorder.BorderBrush,
                                        Background = oldBorder.Background,
                                        Child = childElement
                                    };
                                if (newBorder.Tag is CodeObjectVM)
                                    ((CodeObjectVM)newBorder.Tag).FrameworkElement = newBorder;
                                newBorder.MouseEnter += border_MouseEnter;
                                newBorder.MouseLeave += border_MouseLeave;
                                newBorder.MouseDown += border_MouseDown;
                                AddElementToLineWrapper(newBorder);
                            }
                            else
                                AddElementToLineWrapper(child);
                        }
                        AddElementToLineWrapper(element);
                        element = _currentLineWrapper;
                    }
                }
                _currentLine.Children.Add(element);
            }
        }

        protected void AddElementToCurrentBlock(FrameworkElement element)
        {
            int lastIndex = _currentBlock.Children.Count - 1;
            if (_isNewLine || lastIndex < 0)
            {
                if (element != null)
                    _currentBlock.Children.Add(element);
                else
                    CreateLine(_currentBlock);
            }
            else
            {
                UIElement lastElement = _currentBlock.Children[lastIndex];  // Border or TextBlock
                _currentBlock.Children.RemoveAt(lastIndex);
                CreateLine(_currentBlock);
                AddElementToCurrentLine(lastElement);
                if (element != null)
                    AddElementToCurrentLine(element);
            }
        }

        protected void AddElementToCurrentCanvas(FrameworkElement element, double Y, double X)
        {
            if (element != null)
            {
                // Save the last Canvas index for use by virtualized rendering
                _currentCanvas.Children.Add(element);
                Canvas.SetTop(element, Y);
                if (X > 0)
                    Canvas.SetLeft(element, X);
            }
        }

        protected void AddElementToCurrentBorder(FrameworkElement element)
        {
            if (_currentBorder.Child == null)
            {
                if (element != null)
                    _currentBorder.Child = element;
                else
                    CreateLine(_currentBorder);
            }
            else
            {
                UIElement existingElement = _currentBorder.Child;
                _currentBorder.Child = null;
                if (_isNewLine)
                {
                    CreateBlock(0);
                    _currentBlock.Children.Add(existingElement);
                    if (element != null)
                        _currentBlock.Children.Add(element);
                }
                else
                {
                    if (existingElement is WrapPanel)  // Line
                    {
                        if (element != null)
                            ((Panel)existingElement).Children.Add(element);
                    }
                    else
                    {
                        CreateLine(_currentBorder);
                        AddElementToCurrentLine(existingElement);
                        if (element != null)
                            AddElementToCurrentLine(element);
                    }
                }
            }
        }

        /// <summary>
        /// Return to the parent of the current border.
        /// </summary>
        public void ReturnToBorderParent(CodeObjectVM codeObjectVM)
        {
            NeedsNewLine = false;
            if (Measuring)
            {
                // Pop the current VM, and update the Height & Width if necessary
                MeasureState currentMeasureState = _measureStateStack.Pop();
                CodeObjectVM currentVM = currentMeasureState.CodeObjectVM;
                if (currentVM != codeObjectVM)
                    throw new Exception("ERROR: PopMeasureState: VM doesn't match VM on stack!");
                currentVM.Height += currentMeasureState.LineHeight + (currentMeasureState.VerticalPad * 2);
                if (currentVM.Width < currentMeasureState.LineWidth)
                    currentVM.Width = currentMeasureState.LineWidth;
                currentVM.Width += currentMeasureState.LeftPad + currentMeasureState.RightPad;

                // Update the Height & Width of the parent VM (if any)
                MeasureState parentMeasureState = _measureStateStack.Peek();
                CodeObjectVM parentVM = parentMeasureState.CodeObjectVM;
                if (parentVM != null)
                {
                    if (parentMeasureState.LineHeight < currentVM.Height)
                        parentMeasureState.LineHeight = currentVM.Height;
                    parentMeasureState.LineWidth += currentVM.Width;
                }
            }
            else if (_currentBorder != null)
            {
                DependencyObject parent = _currentBorder.Parent;
                if (parent is InlineUIContainer)
                {
                    _currentLineWrapper = (TextBlock)((InlineUIContainer)parent).Parent;
                    _currentLine = (WrapPanel)_currentLineWrapper.Parent;
                    FindParentBlockOfLine();
                }
                else if (parent is WrapPanel)
                {
                    _currentLineWrapper = null;
                    _currentLine = (WrapPanel)parent;
                    FindParentBlockOfLine();
                }
                else if (parent is StackPanel)
                {
                    _currentLineWrapper = null;
                    _currentLine = null;
                    _currentBlock = (StackPanel)parent;
                    FindParentBorderOfBlock();
                }
                else if (parent is Canvas)
                {
                    _currentLineWrapper = null;
                    _currentLine = null;
                    _currentCanvas = (Canvas)parent;
                    _currentBlock = null;
                    _currentBorder = null;
                }
                else // if (parent is Border)
                {
                    _currentLineWrapper = null;
                    _currentLine = null;
                    _currentBlock = null;
                    _currentBorder = (Border)parent;
                }
            }

            _borderIndentStateStack.Pop();
        }

        /// <summary>
        /// Return to the parent of the current block.
        /// </summary>
        public void ReturnToBlockParent()
        {
            if (_currentBlock != null)
            {
                DependencyObject parent = _currentBlock.Parent;
                if (parent is StackPanel)
                {
                    _currentBlock = (StackPanel)parent;
                    UIElement lastElement = _currentBlock.Children[_currentBlock.Children.Count - 1];
                    _currentLine = (lastElement is WrapPanel ? (WrapPanel)lastElement : null);
                    if (_currentLine != null && _currentLine.Children.Count == 1
                        && _currentLine.Children[0] is TextBlock && ((TextBlock)_currentLine.Children[0]).Inlines.Count > 0)
                        _currentLineWrapper = (TextBlock)_currentLine.Children[0];
                    else
                        _currentLineWrapper = null;
                    FindParentBorderOfBlock();
                }
                // Do nothing if the parent is a Border
            }
        }

        /// <summary>
        /// Return to the parent of the current canvas.
        /// </summary>
        public void ReturnToCanvasParent()
        {
            if (_currentCanvas != null)
            {
                DependencyObject parent = _currentCanvas.Parent;
                if (parent is WrapPanel)
                {
                    _currentLine = (WrapPanel)parent;
                    parent = _currentLine.Parent;
                }
                if (parent is StackPanel)
                {
                    _currentBlock = (StackPanel)parent;
                    FindParentBorderOfBlock();
                }
                if (parent is Border)
                    _currentBorder = (Border)parent;
                _currentCanvas = null;
            }
        }

        protected void FindParentBlockOfLine()
        {
            if (_currentLine.Parent is StackPanel)
            {
                _currentBlock = (StackPanel)_currentLine.Parent;
                FindParentBorderOfBlock();
            }
            else // if (_currentLine.Parent is Border)
            {
                _currentBlock = null;
                _currentBorder = (Border)_currentLine.Parent;
            }
        }

        protected void FindParentBorderOfBlock()
        {
            for (DependencyObject parent = _currentBlock.Parent; ; )
            {
                if (parent is StackPanel)
                {
                    parent = ((StackPanel)parent).Parent;
                    continue;
                }
                if (parent is Border)
                    _currentBorder = (Border)parent;
                break;
            }
        }

        /// <summary>
        /// Get the current absolute Y position.
        /// </summary>
        public double GetCurrentAbsoluteY(CodeObjectVM codeObjectVM)
        {
            MeasureState measureState = _measureStateStack.Peek();
            if (codeObjectVM == null)
                codeObjectVM = measureState.CodeObjectVM;
            else
            {
                while (measureState.CodeObjectVM != codeObjectVM && !(codeObjectVM is CodeUnitVM || codeObjectVM.ParentVM is ProjectVM))
                    codeObjectVM = codeObjectVM.ParentVM;
            }

            // Get the offset within the parent Canvas plus the top of the border and the current height
            double Y = 0;
            if (codeObjectVM != null)
            {
                Y = codeObjectVM.Y + measureState.VerticalPad + codeObjectVM.Height;

                // Add the absolute Y of the parent Canvas
                if (!(codeObjectVM is CodeUnitVM))
                {
                    CodeObjectVM parentBlockVM = codeObjectVM.ParentVM;
                    if (parentBlockVM != null)
                    {
                        BlockVM bodyVM = (parentBlockVM is IBlockVM ? ((IBlockVM)parentBlockVM).BodyVM : null);
                        if (bodyVM == null || !bodyVM.Contains(codeObjectVM) || bodyVM.Y == 0)
                        {
                            // If the parent wasn't an IBlockVM or it's not virtualized (Y is 0), we have to look recursively up
                            // the tree until we find one (this handles BlockVMs in expressions, such as for AnonymousMethodVM,
                            // or the case of a DocCodeVM with a single CodeObjectVM child instead of a BlockVM).   We also must do
                            // this if the CodeObjectVM doesn't exist in the BodyVM of the IBlockVM parent, which can occur if we
                            // came from a prefix, such as a block of code in a DocCodeVM in a DocCommentVM.
                            MeasureState topState = _measureStateStack.Pop();
                            Y += GetCurrentAbsoluteY(parentBlockVM);
                            _measureStateStack.Push(topState);
                        }
                        else
                            Y += bodyVM.Y;
                    }
                }
            }
            return Y;
        }

        /// <summary>
        /// Get the current absolute Y position.
        /// </summary>
        public double GetCurrentAbsoluteY()
        {
            return GetCurrentAbsoluteY(null);
        }

        /// <summary>
        /// Begin a section during which any newline should cause an indent.
        /// </summary>
        public void IndentOnNewLineBegin(CodeObjectVM codeObjectVM, int indentAmount)
        {
            if (indentAmount == 0)
                indentAmount = CodeObject.TabSize;
            BorderIndentState borderIndentState = _borderIndentStateStack.Peek();
            if (borderIndentState.IndentOnNewLine == 0)
                borderIndentState.PendingIndentState = new IndentState(codeObjectVM, indentAmount);
            ++borderIndentState.IndentOnNewLine;
        }

        /// <summary>
        /// Begin a section during which any newline should cause an indent.
        /// </summary>
        public void IndentOnNewLineBegin(CodeObjectVM codeObjectVM)
        {
            IndentOnNewLineBegin(codeObjectVM, 0);
        }

        /// <summary>
        /// End a section during which any newline should cause an indent.
        /// </summary>
        public void IndentOnNewLineEnd(CodeObjectVM codeObjectVM)
        {
            BorderIndentState borderIndentState = _borderIndentStateStack.Peek();
            if (borderIndentState.IndentOnNewLine > 0)
                --borderIndentState.IndentOnNewLine;
            else if (borderIndentState.IndentStateStack != null && borderIndentState.IndentStateStack.Count > 0)
            {
                if (borderIndentState.IndentStateStack.Peek().CodeObjectVM == codeObjectVM)
                    borderIndentState.IndentStateStack.Pop();
            }
        }

        /// <summary>
        /// Turn off indentation on new lines for the current level.
        /// </summary>
        public void IndentOnNewLineReset()
        {
            BorderIndentState borderIndentState = _borderIndentStateStack.Peek();
            borderIndentState.IndentOnNewLine = 0;
        }

        /// <summary>
        /// Render an EOL comment.
        /// </summary>
        /// <remarks>
        /// Unlike the CodeWriter for text output, we don't do deferred rendering of EOL comments because it gets too ugly with UI objects,
        /// and it's also not technically necessary (the format in this case is only cosmetic).  As with text, the comments will render with
        /// '//' by default, or retain '/* */' if parsed/marked as block comments.  Auto-cleanup might convert the latter to the former, and
        /// code editing will also convert between them as necessary.  Accidentally using '//' when a non-comment follows won't cause any
        /// problems in the GUI, unlike with text where it would be invalid.
        /// </remarks>
        public void RenderEOLComment(CommentVM commentVM)
        {
            // Preserve 'NeedsNewLine' state and clear it during this operation
            bool needsNewLine = NeedsNewLine;
            NeedsNewLine = false;
            commentVM.Render(this, CodeObjectVM.RenderFlags.None);
            NeedsNewLine = needsNewLine;
        }

        #endregion

        #region /* ACTIVATION (MOUSE-OVER) */

        protected const int InitialDelay = 400;
        protected const int BetweenDelay = 150;

        protected int _delay = InitialDelay;
        protected TextBlock _activeTextBlock;
        protected Border _activeBorder;
        protected readonly List<Border> _inBorders = new List<Border>();
        protected Brush _oldBorderBrush;
        protected Image _activeImage;
        protected CodeObjectVM _activeCodeObjectVM;
        protected DispatcherTimer _toolTipTimer;
        protected Popup _toolTipPopup;
        protected CodeRenderer _toolTipRenderer;

        protected static DropShadowEffect DropShadowEffect = new DropShadowEffect { BlurRadius = 2, Color = Colors.DarkGray, Direction = 0, ShadowDepth = 0 };

        protected void InitializeToolTipTimer()
        {
            _delay = InitialDelay;
            _toolTipTimer = new DispatcherTimer();
            _toolTipTimer.Tick += toolTipTimerElapsed;
        }

        public void ResetToolTipTimer()
        {
            _toolTipTimer.Stop();
            _toolTipTimer.Interval = new TimeSpan(0, 0, 0, 0, _delay);
            _toolTipTimer.Start();
        }

        public void StopToolTipTimer()
        {
            _toolTipTimer.Stop();
        }

        protected void ActivateText(TextBlock textBlock, bool isMouseDirectlyOver)
        {
            DeactivateText();
            _activeTextBlock = textBlock;
            if (isMouseDirectlyOver)
                _activeCodeObjectVM = textBlock.Tag as CodeObjectVM;

            // Apply a shadow effect to show activation
            textBlock.Effect = DropShadowEffect;
        }

        protected void DeactivateText()
        {
            if (_activeTextBlock != null)
            {
                _activeTextBlock.Effect = null;
                _activeTextBlock = null;
            }
        }

        protected void ActivateBorder(Border border, bool isMouseDirectlyOver)
        {
            DeactivateBorder();
            _activeBorder = border;
            if (isMouseDirectlyOver)
                _activeCodeObjectVM = (CodeObjectVM)border.Tag;

            // Change the border line to show activation (bitmap effects are too slow for this)
            _oldBorderBrush = border.BorderBrush;
            border.BorderBrush = Brushes.Blue;
        }

        protected void DeactivateBorder()
        {
            if (_activeBorder != null)
            {
                _activeBorder.BorderBrush = _oldBorderBrush;
                _activeBorder = null;
            }
        }

        protected void ActivateImage(Image image, bool isMouseDirectlyOver)
        {
            DeactivateImage();
            _activeImage = image;
            if (isMouseDirectlyOver)
                _activeCodeObjectVM = (CodeObjectVM)image.Tag;

            // Add a dropshadow effect
            image.Effect = new DropShadowEffect();
        }

        protected void DeactivateImage()
        {
            if (_activeImage != null)
            {
                _activeImage.Effect = null;
                _activeImage = null;
            }
        }

        public void DisplayToolTip()
        {
            UIElement element = (_activeImage ?? (UIElement)_activeTextBlock ?? _activeBorder);
            if (element != null)
            {
                StackPanel panel = new StackPanel { Tag = _activeCodeObjectVM };
                Border border = new Border
                    {
                        HorizontalAlignment = HorizontalAlignment.Left,
                        VerticalAlignment = VerticalAlignment.Center,
                        SnapsToDevicePixels = true, // Needed to prevent anti-aliasing
                        CornerRadius = new CornerRadius(_zoomLevel * 4),
                        BorderThickness = new Thickness(_zoomLevel),
                        Padding = new Thickness(_zoomLevel * 3),
                        Margin = new Thickness(0, 0, 4, 4), // Leave margin for drop shadow
                        Effect = new DropShadowEffect { Color = Colors.Gray, Opacity = 0.4, ShadowDepth = 4 },
                        BorderBrush = new SolidColorBrush(Colors.Gray),
                        Background = new LinearGradientBrush(
                            new GradientStopCollection(3)
                                {
                                    new GradientStop(Colors.WhiteSmoke, 0),
                                    new GradientStop(Colors.White, 0.4),
                                    new GradientStop(Colors.WhiteSmoke, 1)
                                },
                            new Point(0.5, 0), new Point(0.5, 1)),
                        Child = panel,
                        Tag = _activeCodeObjectVM
                    };

                Point offset = Mouse.GetPosition(element);
                _toolTipPopup = new Popup
                    {
                        AllowsTransparency = true,
                        IsOpen = true,
                        PlacementTarget = element,
                        Placement = PlacementMode.Relative,
                        HorizontalOffset = offset.X + 8,
                        VerticalOffset = offset.Y + 10,
                        Child = border,
                        Tag = _activeCodeObjectVM
                    };
                foreach (object commandBinding in CommandBindings)
                    _toolTipPopup.CommandBindings.Add((CommandBinding)commandBinding);
                _toolTipPopup.ContextMenuOpening += ToolTipContextMenuOpening;

                _toolTipRenderer = new CodeRenderer(panel) { ToolTipContextMenuOpening = ToolTipContextMenuOpening, CommandBindings = CommandBindings};

#if SHOW_MEASUREMENTS
                if (element is Border)
                    renderer.RenderNameValue("Border Height-Width", string.Format("{0:F2} - {1:F2}", element.ActualHeight, element.ActualWidth));
                renderer.RenderNameValue("Height-Width", string.Format("{0:F2} - {1:F2}, Y={2:F2}", codeObjectVM.Height, codeObjectVM.Width, codeObjectVM.Y));
                if (codeObjectVM is IBlockVM)
                {
                    BlockVM blockVM = ((IBlockVM)codeObjectVM).BodyVM;
                    if (blockVM != null)
                        renderer.RenderNameValue("Block Height-Width", string.Format("{0:F2} - {1:F2}, Y={2:F2}", blockVM.Height, blockVM.Width, blockVM.Y));
                }
                renderer.NewLine();
#endif
                // Create a new VM to render the tooltip, because the height/width will differ for a description
                CodeObjectVM.CreateVM(_activeCodeObjectVM.CodeObject, _activeCodeObjectVM.ParentVM, true).RenderToolTip(_toolTipRenderer);

                _delay = BetweenDelay;
            }
        }

        protected void ClearActives()
        {
            _activeTextBlock = null;
            _activeBorder = null;
            _activeCodeObjectVM = null;
        }

        public void CloseToolTip(bool resetTimer, bool clearActives)
        {
            if (_toolTipPopup != null)
            {
                _toolTipRenderer.CloseToolTip(false, true);
                _toolTipPopup.IsOpen = false;
                _toolTipPopup = null;
                _delay = InitialDelay;
            }
            if (resetTimer)
                ResetToolTipTimer();
            if (clearActives)
                ClearActives();
        }

        public void CloseToolTip()
        {
            CloseToolTip(false, false);
        }

        #endregion

        #region /* EVENTS */

        protected void textBlock_MouseEnter(object sender, MouseEventArgs e)
        {
            e.Handled = true;
            TextBlock textBlock = (TextBlock)sender;

            // Upon entering a text block, clear any other active objects and make
            // the text block active.  We should never have nested text blocks.
            ActivateText(textBlock, textBlock.IsMouseDirectlyOver);
        }

        protected void textBlock_MouseLeave(object sender, MouseEventArgs e)
        {
            e.Handled = true;
            TextBlock textBlock = (TextBlock)sender;

            // Upon exiting a text block, deactivate any currently active text block IF it's
            // the same one (should always be, but check to be sure).
            if (textBlock == _activeTextBlock)
            {
                DeactivateText();

                // Set the active object to that of any parent border
                if (_activeBorder != null)
                    _activeCodeObjectVM = _activeBorder.Tag as CodeObjectVM;
            }
        }

        protected void textBlock_MouseDown(object sender, MouseEventArgs e)
        {
            e.Handled = true;
            CloseToolTip();
        }

        protected void border_MouseEnter(object sender, MouseEventArgs e)
        {
            e.Handled = true;
            Border border = (Border)sender;
            _inBorders.Add(border);  // Maintain list of borders we are inside
            if (_activeBorder == null || border.ActualWidth < _activeBorder.ActualWidth)
                ActivateBorder(border, border.IsMouseDirectlyOver);
        }

        protected void border_MouseLeave(object sender, MouseEventArgs e)
        {
            e.Handled = true;
            Border border = (Border)sender;
            _inBorders.Remove(border);  // update list of borders we are inside
            if (border == _activeBorder)
            {
                DeactivateBorder();

                // Find and activate the next outer border, or close the tooltip if the mouse isn't over it
                // (meaning we've gone outside of the code window area).
                Border top = null;
                foreach (Border b in _inBorders)
                {
                    if (top == null || b.ActualWidth < top.ActualWidth)
                        top = b;
                }
                if (top != null)
                    ActivateBorder(top, top.IsMouseDirectlyOver);
            }
        }

        protected void border_MouseDown(object sender, MouseEventArgs e)
        {
            e.Handled = true;
            CloseToolTip();
        }

        protected void image_MouseEnter(object sender, MouseEventArgs e)
        {
            e.Handled = true;
            Image image = (Image)sender;

            // Upon entering an image, clear any other active objects and make
            // the image active.
            ActivateImage(image, image.IsMouseDirectlyOver);
        }

        protected void image_MouseLeave(object sender, MouseEventArgs e)
        {
            e.Handled = true;
            Image image = (Image)sender;

            // Upon exiting an image, deactivate any currently active image IF it's
            // the same one (should always be, but check to be sure).
            if (image == _activeImage)
            {
                DeactivateImage();

                // Set the active object to that of any parent border
                if (_activeBorder != null)
                    _activeCodeObjectVM = _activeBorder.Tag as CodeObjectVM;
            }
        }

        protected void stackPanel_MouseMove(object sender, MouseEventArgs e)
        {
            ResetToolTipTimer();
        }

        protected void toolTipTimerElapsed(object sender, EventArgs e)
        {
            _toolTipTimer.Stop();

            // Ignore if the mouse is over the tooltip
            if (_toolTipPopup == null || !_toolTipPopup.IsMouseOver)
            {
                // Close any existing tooltip
                if (_toolTipPopup != null)
                    CloseToolTip();
                if (_toolTipPopup == null && _activeCodeObjectVM != null)
                {
                    // Open a new tooltip if appropriate
                    CodeObjectVM topMostVM = _topMost.Tag as CodeObjectVM;
                    if (topMostVM == null || topMostVM.CodeObject != _activeCodeObjectVM.CodeObject)
                        DisplayToolTip();
                }
            }
        }

        #endregion

        #region /* COMMANDS */

        public static readonly RoutedCommand ShowBordersCommand = new RoutedCommand("Show Borders", typeof(CodeObjectVM));
        public static readonly RoutedCommand ShowBackgroundColorsCommand = new RoutedCommand("Show Background Colors", typeof(CodeObjectVM));
        public static readonly RoutedCommand UseShadingCommand = new RoutedCommand("Use Shading", typeof(CodeObjectVM));
        public static readonly RoutedCommand HalfHeightBlankLinesCommand = new RoutedCommand("Half-height Blank Lines", typeof(CodeObjectVM));
        public static readonly RoutedCommand MaximizeBordersCommand = new RoutedCommand("Maximize Use Of Borders", typeof(CodeObjectVM));

        private static void InitializeCommands()
        {
            //ShowBackgroundColorsCommand.InputGestures.Add(new KeyGesture(Key.O, ModifierKeys.Alt));
        }

        public static void BindCommands(Window window)
        {
            WPFUtil.AddCommandBinding(window, ShowBordersCommand, showBorders_Executed);
            WPFUtil.AddCommandBinding(window, ShowBackgroundColorsCommand, showBackgroundColors_Executed);
            WPFUtil.AddCommandBinding(window, UseShadingCommand, useShading_Executed);
            WPFUtil.AddCommandBinding(window, HalfHeightBlankLinesCommand, halfHeightBlankLines_Executed);
            WPFUtil.AddCommandBinding(window, MaximizeBordersCommand, maximizeBorders_Executed);
        }

        private static void showBorders_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            ShowBorders = !ShowBorders;
            ReRenderAll();
        }

        private static void showBackgroundColors_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            ShowBackgroundColors = !ShowBackgroundColors;
            ReRenderAll();
        }

        private static void useShading_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            UseShading = !UseShading;
            ReRenderAll();
        }

        private static void halfHeightBlankLines_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            HalfHeightBlankLines = !HalfHeightBlankLines;
            ReRenderAll();
        }

        private static void maximizeBorders_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            MaximizeBorders = !MaximizeBorders;
            ReRenderAll();
        }

        #endregion

        #region /* INDENT STATE */

        /// <summary>
        /// State information for indentation within the current border.
        /// </summary>
        protected class BorderIndentState
        {
            public int IndentOnNewLine;
            public CodeObjectVM BaseCodeObjectVM;
            public IndentState PendingIndentState;
            public Stack<IndentState> IndentStateStack = new Stack<IndentState>();

            public BorderIndentState(CodeObjectVM baseCodeObjectVM)
            {
                BaseCodeObjectVM = baseCodeObjectVM;
            }
        }

        /// <summary>
        /// State information for indentation within the current border.
        /// </summary>
        protected class IndentState
        {
            public CodeObjectVM CodeObjectVM;
            public int IndentPosition;

            public IndentState(CodeObjectVM codeObjectVM, int indentPosition)
            {
                CodeObjectVM = codeObjectVM;
                IndentPosition = indentPosition;
            }
        }

        #endregion
        
        #region /* MEASURE STATE */

        /// <summary>
        /// Measurement information for each nested <see cref="CodeObjectVM"/> being rendered.
        /// </summary>
        protected class MeasureState
        {
            public CodeObjectVM CodeObjectVM;
            public double LeftPad;
            public double RightPad;
            public double VerticalPad;
            public double LineHeight;
            public double LineWidth;
            public double MaxBaseline;
            public double MaxUnderBaseline;

            public MeasureState(CodeObjectVM codeObjectVM, double leftPad, double rightPad, double verticalPad)
            {
                CodeObjectVM = codeObjectVM;
                LeftPad = leftPad;
                RightPad = rightPad;
                VerticalPad = verticalPad;
            }
        }

        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Common Development and Distribution License (CDDL)


Written By
Software Developer (Senior)
United States United States
I've been writing software since the late 70's, currently focusing mainly on C#.NET. I also like to travel around the world, and I own a Chocolate Factory (sadly, none of my employees are oompa loompas).

Comments and Discussions