// 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
}
}