// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
using System.Windows.Threading;
using ICSharpCode.NRefactory.Editor;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Rendering
{
///
/// A virtualizing panel producing+showing s for a .
///
/// This is the heart of the text editor, this class controls the text rendering process.
///
/// Taken as a standalone control, it's a text viewer without any editing capability.
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable",
Justification = "The user usually doesn't work with TextView but with TextEditor; and nulling the Document property is sufficient to dispose everything.")]
public class TextView : FrameworkElement, IScrollInfo, IWeakEventListener, ITextEditorComponent, IServiceProvider
{
#region Constructor
static TextView()
{
ClipToBoundsProperty.OverrideMetadata(typeof(TextView), new FrameworkPropertyMetadata(Boxes.True));
FocusableProperty.OverrideMetadata(typeof(TextView), new FrameworkPropertyMetadata(Boxes.False));
}
ColumnRulerRenderer columnRulerRenderer;
CurrentLineHighlightRenderer currentLineHighlighRenderer;
///
/// Creates a new TextView instance.
///
public TextView()
{
services.AddService(typeof(TextView), this);
textLayer = new TextLayer(this);
elementGenerators = new ObserveAddRemoveCollection(ElementGenerator_Added, ElementGenerator_Removed);
lineTransformers = new ObserveAddRemoveCollection(LineTransformer_Added, LineTransformer_Removed);
backgroundRenderers = new ObserveAddRemoveCollection(BackgroundRenderer_Added, BackgroundRenderer_Removed);
columnRulerRenderer = new ColumnRulerRenderer(this);
currentLineHighlighRenderer = new CurrentLineHighlightRenderer(this);
this.Options = new TextEditorOptions();
Debug.Assert(singleCharacterElementGenerator != null); // assert that the option change created the builtin element generators
layers = new LayerCollection(this);
InsertLayer(textLayer, KnownLayer.Text, LayerInsertionPosition.Replace);
this.hoverLogic = new MouseHoverLogic(this);
this.hoverLogic.MouseHover += (sender, e) => RaiseHoverEventPair(e, PreviewMouseHoverEvent, MouseHoverEvent);
this.hoverLogic.MouseHoverStopped += (sender, e) => RaiseHoverEventPair(e, PreviewMouseHoverStoppedEvent, MouseHoverStoppedEvent);
}
#endregion
#region Document Property
///
/// Document property.
///
public static readonly DependencyProperty DocumentProperty =
DependencyProperty.Register("Document", typeof(TextDocument), typeof(TextView),
new FrameworkPropertyMetadata(OnDocumentChanged));
TextDocument document;
HeightTree heightTree;
///
/// Gets/Sets the document displayed by the text editor.
///
public TextDocument Document {
get { return (TextDocument)GetValue(DocumentProperty); }
set { SetValue(DocumentProperty, value); }
}
static void OnDocumentChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
{
((TextView)dp).OnDocumentChanged((TextDocument)e.OldValue, (TextDocument)e.NewValue);
}
internal double FontSize {
get {
return (double)GetValue(TextBlock.FontSizeProperty);
}
}
///
/// Occurs when the document property has changed.
///
public event EventHandler DocumentChanged;
void OnDocumentChanged(TextDocument oldValue, TextDocument newValue)
{
if (oldValue != null) {
heightTree.Dispose();
heightTree = null;
formatter.Dispose();
formatter = null;
cachedElements.Dispose();
cachedElements = null;
TextDocumentWeakEventManager.Changing.RemoveListener(oldValue, this);
}
this.document = newValue;
ClearScrollData();
ClearVisualLines();
if (newValue != null) {
TextDocumentWeakEventManager.Changing.AddListener(newValue, this);
formatter = TextFormatterFactory.Create(this);
InvalidateDefaultTextMetrics(); // measuring DefaultLineHeight depends on formatter
heightTree = new HeightTree(newValue, DefaultLineHeight);
cachedElements = new TextViewCachedElements();
}
InvalidateMeasure(DispatcherPriority.Normal);
if (DocumentChanged != null)
DocumentChanged(this, EventArgs.Empty);
}
///
/// Recreates the text formatter that is used internally
/// by calling .
///
void RecreateTextFormatter()
{
if (formatter != null) {
formatter.Dispose();
formatter = TextFormatterFactory.Create(this);
Redraw();
}
}
void RecreateCachedElements()
{
if (cachedElements != null) {
cachedElements.Dispose();
cachedElements = new TextViewCachedElements();
}
}
///
protected virtual bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
if (managerType == typeof(TextDocumentWeakEventManager.Changing)) {
// TODO: put redraw into background so that other input events can be handled before the redraw.
// Unfortunately the "easy" approach (just use DispatcherPriority.Background) here makes the editor twice as slow because
// the caret position change forces an immediate redraw, and the text input then forces a background redraw.
// When fixing this, make sure performance on the SharpDevelop "type text in C# comment" stress test doesn't get significantly worse.
DocumentChangeEventArgs change = (DocumentChangeEventArgs)e;
Redraw(change.Offset, change.RemovalLength, DispatcherPriority.Normal);
return true;
} else if (managerType == typeof(PropertyChangedWeakEventManager)) {
OnOptionChanged((PropertyChangedEventArgs)e);
return true;
}
return false;
}
bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
return ReceiveWeakEvent(managerType, sender, e);
}
#endregion
#region Options property
///
/// Options property.
///
public static readonly DependencyProperty OptionsProperty =
DependencyProperty.Register("Options", typeof(TextEditorOptions), typeof(TextView),
new FrameworkPropertyMetadata(OnOptionsChanged));
///
/// Gets/Sets the options used by the text editor.
///
public TextEditorOptions Options {
get { return (TextEditorOptions)GetValue(OptionsProperty); }
set { SetValue(OptionsProperty, value); }
}
///
/// Occurs when a text editor option has changed.
///
public event PropertyChangedEventHandler OptionChanged;
///
/// Raises the event.
///
protected virtual void OnOptionChanged(PropertyChangedEventArgs e)
{
if (OptionChanged != null) {
OptionChanged(this, e);
}
if (Options.ShowColumnRuler)
columnRulerRenderer.SetRuler(Options.ColumnRulerPosition, ColumnRulerPen);
else
columnRulerRenderer.SetRuler(-1, ColumnRulerPen);
UpdateBuiltinElementGeneratorsFromOptions();
Redraw();
}
static void OnOptionsChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
{
((TextView)dp).OnOptionsChanged((TextEditorOptions)e.OldValue, (TextEditorOptions)e.NewValue);
}
void OnOptionsChanged(TextEditorOptions oldValue, TextEditorOptions newValue)
{
if (oldValue != null) {
PropertyChangedWeakEventManager.RemoveListener(oldValue, this);
}
if (newValue != null) {
PropertyChangedWeakEventManager.AddListener(newValue, this);
}
OnOptionChanged(new PropertyChangedEventArgs(null));
}
#endregion
#region ElementGenerators+LineTransformers Properties
readonly ObserveAddRemoveCollection elementGenerators;
///
/// Gets a collection where element generators can be registered.
///
public IList ElementGenerators {
get { return elementGenerators; }
}
void ElementGenerator_Added(VisualLineElementGenerator generator)
{
ConnectToTextView(generator);
Redraw();
}
void ElementGenerator_Removed(VisualLineElementGenerator generator)
{
DisconnectFromTextView(generator);
Redraw();
}
readonly ObserveAddRemoveCollection lineTransformers;
///
/// Gets a collection where line transformers can be registered.
///
public IList LineTransformers {
get { return lineTransformers; }
}
void LineTransformer_Added(IVisualLineTransformer lineTransformer)
{
ConnectToTextView(lineTransformer);
Redraw();
}
void LineTransformer_Removed(IVisualLineTransformer lineTransformer)
{
DisconnectFromTextView(lineTransformer);
Redraw();
}
#endregion
#region Builtin ElementGenerators
// NewLineElementGenerator newLineElementGenerator;
SingleCharacterElementGenerator singleCharacterElementGenerator;
LinkElementGenerator linkElementGenerator;
MailLinkElementGenerator mailLinkElementGenerator;
void UpdateBuiltinElementGeneratorsFromOptions()
{
TextEditorOptions options = this.Options;
// AddRemoveDefaultElementGeneratorOnDemand(ref newLineElementGenerator, options.ShowEndOfLine);
AddRemoveDefaultElementGeneratorOnDemand(ref singleCharacterElementGenerator, options.ShowBoxForControlCharacters || options.ShowSpaces || options.ShowTabs);
AddRemoveDefaultElementGeneratorOnDemand(ref linkElementGenerator, options.EnableHyperlinks);
AddRemoveDefaultElementGeneratorOnDemand(ref mailLinkElementGenerator, options.EnableEmailHyperlinks);
}
void AddRemoveDefaultElementGeneratorOnDemand(ref T generator, bool demand)
where T : VisualLineElementGenerator, IBuiltinElementGenerator, new()
{
bool hasGenerator = generator != null;
if (hasGenerator != demand) {
if (demand) {
generator = new T();
this.ElementGenerators.Add(generator);
} else {
this.ElementGenerators.Remove(generator);
generator = null;
}
}
if (generator != null)
generator.FetchOptions(this.Options);
}
#endregion
#region Layers
internal readonly TextLayer textLayer;
readonly LayerCollection layers;
///
/// Gets the list of layers displayed in the text view.
///
public UIElementCollection Layers {
get { return layers; }
}
sealed class LayerCollection : UIElementCollection
{
readonly TextView textView;
public LayerCollection(TextView textView)
: base(textView, textView)
{
this.textView = textView;
}
public override void Clear()
{
base.Clear();
textView.LayersChanged();
}
public override int Add(UIElement element)
{
int r = base.Add(element);
textView.LayersChanged();
return r;
}
public override void RemoveAt(int index)
{
base.RemoveAt(index);
textView.LayersChanged();
}
public override void RemoveRange(int index, int count)
{
base.RemoveRange(index, count);
textView.LayersChanged();
}
}
void LayersChanged()
{
textLayer.index = layers.IndexOf(textLayer);
}
///
/// Inserts a new layer at a position specified relative to an existing layer.
///
/// The new layer to insert.
/// The existing layer
/// Specifies whether the layer is inserted above,below, or replaces the referenced layer
public void InsertLayer(UIElement layer, KnownLayer referencedLayer, LayerInsertionPosition position)
{
if (layer == null)
throw new ArgumentNullException("layer");
if (!Enum.IsDefined(typeof(KnownLayer), referencedLayer))
throw new InvalidEnumArgumentException("referencedLayer", (int)referencedLayer, typeof(KnownLayer));
if (!Enum.IsDefined(typeof(LayerInsertionPosition), position))
throw new InvalidEnumArgumentException("position", (int)position, typeof(LayerInsertionPosition));
if (referencedLayer == KnownLayer.Background && position != LayerInsertionPosition.Above)
throw new InvalidOperationException("Cannot replace or insert below the background layer.");
LayerPosition newPosition = new LayerPosition(referencedLayer, position);
LayerPosition.SetLayerPosition(layer, newPosition);
for (int i = 0; i < layers.Count; i++) {
LayerPosition p = LayerPosition.GetLayerPosition(layers[i]);
if (p != null) {
if (p.KnownLayer == referencedLayer && p.Position == LayerInsertionPosition.Replace) {
// found the referenced layer
switch (position) {
case LayerInsertionPosition.Below:
layers.Insert(i, layer);
return;
case LayerInsertionPosition.Above:
layers.Insert(i + 1, layer);
return;
case LayerInsertionPosition.Replace:
layers[i] = layer;
return;
}
} else if (p.KnownLayer == referencedLayer && p.Position == LayerInsertionPosition.Above
|| p.KnownLayer > referencedLayer) {
// we skipped the insertion position (referenced layer does not exist?)
layers.Insert(i, layer);
return;
}
}
}
// inserting after all existing layers:
layers.Add(layer);
}
///
protected override int VisualChildrenCount {
get { return layers.Count + inlineObjects.Count; }
}
///
protected override Visual GetVisualChild(int index)
{
int cut = textLayer.index + 1;
if (index < cut)
return layers[index];
else if (index < cut + inlineObjects.Count)
return inlineObjects[index - cut].Element;
else
return layers[index - inlineObjects.Count];
}
///
protected override System.Collections.IEnumerator LogicalChildren {
get {
return inlineObjects.Select(io => io.Element).Concat(layers.Cast()).GetEnumerator();
}
}
#endregion
#region Inline object handling
List inlineObjects = new List();
///
/// Adds a new inline object.
///
internal void AddInlineObject(InlineObjectRun inlineObject)
{
Debug.Assert(inlineObject.VisualLine != null);
// Remove inline object if its already added, can happen e.g. when recreating textrun for word-wrapping
bool alreadyAdded = false;
for (int i = 0; i < inlineObjects.Count; i++) {
if (inlineObjects[i].Element == inlineObject.Element) {
RemoveInlineObjectRun(inlineObjects[i], true);
inlineObjects.RemoveAt(i);
alreadyAdded = true;
break;
}
}
inlineObjects.Add(inlineObject);
if (!alreadyAdded) {
AddVisualChild(inlineObject.Element);
}
inlineObject.Element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
inlineObject.desiredSize = inlineObject.Element.DesiredSize;
}
void MeasureInlineObjects()
{
// As part of MeasureOverride(), re-measure the inline objects
foreach (InlineObjectRun inlineObject in inlineObjects) {
if (inlineObject.VisualLine.IsDisposed) {
// Don't re-measure inline objects that are going to be removed anyways.
// If the inline object will be reused in a different VisualLine, we'll measure it in the AddInlineObject() call.
continue;
}
inlineObject.Element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
if (!inlineObject.Element.DesiredSize.IsClose(inlineObject.desiredSize)) {
// the element changed size -> recreate its parent visual line
inlineObject.desiredSize = inlineObject.Element.DesiredSize;
if (allVisualLines.Remove(inlineObject.VisualLine)) {
DisposeVisualLine(inlineObject.VisualLine);
}
}
}
}
List visualLinesWithOutstandingInlineObjects = new List();
void RemoveInlineObjects(VisualLine visualLine)
{
// Delay removing inline objects:
// A document change immediately invalidates affected visual lines, but it does not
// cause an immediate redraw.
// To prevent inline objects from flickering when they are recreated, we delay removing
// inline objects until the next redraw.
if (visualLine.hasInlineObjects) {
visualLinesWithOutstandingInlineObjects.Add(visualLine);
}
}
///
/// Remove the inline objects that were marked for removal.
///
void RemoveInlineObjectsNow()
{
if (visualLinesWithOutstandingInlineObjects.Count == 0)
return;
inlineObjects.RemoveAll(
ior => {
if (visualLinesWithOutstandingInlineObjects.Contains(ior.VisualLine)) {
RemoveInlineObjectRun(ior, false);
return true;
}
return false;
});
visualLinesWithOutstandingInlineObjects.Clear();
}
// Remove InlineObjectRun.Element from TextLayer.
// Caller of RemoveInlineObjectRun will remove it from inlineObjects collection.
void RemoveInlineObjectRun(InlineObjectRun ior, bool keepElement)
{
if (!keepElement && ior.Element.IsKeyboardFocusWithin) {
// When the inline element that has the focus is removed, WPF will reset the
// focus to the main window without raising appropriate LostKeyboardFocus events.
// To work around this, we manually set focus to the next focusable parent.
UIElement element = this;
while (element != null && !element.Focusable) {
element = VisualTreeHelper.GetParent(element) as UIElement;
}
if (element != null)
Keyboard.Focus(element);
}
ior.VisualLine = null;
if (!keepElement)
RemoveVisualChild(ior.Element);
}
#endregion
#region Brushes
///
/// NonPrintableCharacterBrush dependency property.
///
public static readonly DependencyProperty NonPrintableCharacterBrushProperty =
DependencyProperty.Register("NonPrintableCharacterBrush", typeof(Brush), typeof(TextView),
new FrameworkPropertyMetadata(Brushes.LightGray));
///
/// Gets/sets the Brush used for displaying non-printable characters.
///
public Brush NonPrintableCharacterBrush {
get { return (Brush)GetValue(NonPrintableCharacterBrushProperty); }
set { SetValue(NonPrintableCharacterBrushProperty, value); }
}
///
/// LinkTextForegroundBrush dependency property.
///
public static readonly DependencyProperty LinkTextForegroundBrushProperty =
DependencyProperty.Register("LinkTextForegroundBrush", typeof(Brush), typeof(TextView),
new FrameworkPropertyMetadata(Brushes.Blue));
///
/// Gets/sets the Brush used for displaying link texts.
///
public Brush LinkTextForegroundBrush {
get { return (Brush)GetValue(LinkTextForegroundBrushProperty); }
set { SetValue(LinkTextForegroundBrushProperty, value); }
}
///
/// LinkTextBackgroundBrush dependency property.
///
public static readonly DependencyProperty LinkTextBackgroundBrushProperty =
DependencyProperty.Register("LinkTextBackgroundBrush", typeof(Brush), typeof(TextView),
new FrameworkPropertyMetadata(Brushes.Transparent));
///
/// Gets/sets the Brush used for the background of link texts.
///
public Brush LinkTextBackgroundBrush {
get { return (Brush)GetValue(LinkTextBackgroundBrushProperty); }
set { SetValue(LinkTextBackgroundBrushProperty, value); }
}
#endregion
#region Redraw methods / VisualLine invalidation
///
/// Causes the text editor to regenerate all visual lines.
///
public void Redraw()
{
Redraw(DispatcherPriority.Normal);
}
///
/// Causes the text editor to regenerate all visual lines.
///
public void Redraw(DispatcherPriority redrawPriority)
{
VerifyAccess();
ClearVisualLines();
InvalidateMeasure(redrawPriority);
}
///
/// Causes the text editor to regenerate the specified visual line.
///
public void Redraw(VisualLine visualLine, DispatcherPriority redrawPriority = DispatcherPriority.Normal)
{
VerifyAccess();
if (allVisualLines.Remove(visualLine)) {
DisposeVisualLine(visualLine);
InvalidateMeasure(redrawPriority);
}
}
///
/// Causes the text editor to redraw all lines overlapping with the specified segment.
///
public void Redraw(int offset, int length, DispatcherPriority redrawPriority = DispatcherPriority.Normal)
{
VerifyAccess();
bool changedSomethingBeforeOrInLine = false;
for (int i = 0; i < allVisualLines.Count; i++) {
VisualLine visualLine = allVisualLines[i];
int lineStart = visualLine.FirstDocumentLine.Offset;
int lineEnd = visualLine.LastDocumentLine.Offset + visualLine.LastDocumentLine.TotalLength;
if (offset <= lineEnd) {
changedSomethingBeforeOrInLine = true;
if (offset + length >= lineStart) {
allVisualLines.RemoveAt(i--);
DisposeVisualLine(visualLine);
}
}
}
if (changedSomethingBeforeOrInLine) {
// Repaint not only when something in visible area was changed, but also when anything in front of it
// was changed. We might have to redraw the line number margin. Or the highlighting changed.
// However, we'll try to reuse the existing VisualLines.
InvalidateMeasure(redrawPriority);
}
}
///
/// Causes a known layer to redraw.
/// This method does not invalidate visual lines;
/// use the method to do that.
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "knownLayer",
Justification="This method is meant to invalidate only a specific layer - I just haven't figured out how to do that, yet.")]
public void InvalidateLayer(KnownLayer knownLayer)
{
InvalidateMeasure(DispatcherPriority.Normal);
}
///
/// Causes a known layer to redraw.
/// This method does not invalidate visual lines;
/// use the method to do that.
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "knownLayer",
Justification="This method is meant to invalidate only a specific layer - I just haven't figured out how to do that, yet.")]
public void InvalidateLayer(KnownLayer knownLayer, DispatcherPriority priority)
{
InvalidateMeasure(priority);
}
///
/// Causes the text editor to redraw all lines overlapping with the specified segment.
/// Does nothing if segment is null.
///
public void Redraw(ISegment segment, DispatcherPriority redrawPriority = DispatcherPriority.Normal)
{
if (segment != null) {
Redraw(segment.Offset, segment.Length, redrawPriority);
}
}
///
/// Invalidates all visual lines.
/// The caller of ClearVisualLines() must also call InvalidateMeasure() to ensure
/// that the visual lines will be recreated.
///
void ClearVisualLines()
{
visibleVisualLines = null;
if (allVisualLines.Count != 0) {
foreach (VisualLine visualLine in allVisualLines) {
DisposeVisualLine(visualLine);
}
allVisualLines.Clear();
}
}
void DisposeVisualLine(VisualLine visualLine)
{
if (newVisualLines != null && newVisualLines.Contains(visualLine)) {
throw new ArgumentException("Cannot dispose visual line because it is in construction!");
}
visibleVisualLines = null;
visualLine.Dispose();
RemoveInlineObjects(visualLine);
}
#endregion
#region InvalidateMeasure(DispatcherPriority)
DispatcherOperation invalidateMeasureOperation;
void InvalidateMeasure(DispatcherPriority priority)
{
if (priority >= DispatcherPriority.Render) {
if (invalidateMeasureOperation != null) {
invalidateMeasureOperation.Abort();
invalidateMeasureOperation = null;
}
base.InvalidateMeasure();
} else {
if (invalidateMeasureOperation != null) {
invalidateMeasureOperation.Priority = priority;
} else {
invalidateMeasureOperation = Dispatcher.BeginInvoke(
priority,
new Action(
delegate {
invalidateMeasureOperation = null;
base.InvalidateMeasure();
}
)
);
}
}
}
#endregion
#region Get(OrConstruct)VisualLine
///
/// Gets the visual line that contains the document line with the specified number.
/// Returns null if the document line is outside the visible range.
///
public VisualLine GetVisualLine(int documentLineNumber)
{
// TODO: EnsureVisualLines() ?
foreach (VisualLine visualLine in allVisualLines) {
Debug.Assert(visualLine.IsDisposed == false);
int start = visualLine.FirstDocumentLine.LineNumber;
int end = visualLine.LastDocumentLine.LineNumber;
if (documentLineNumber >= start && documentLineNumber <= end)
return visualLine;
}
return null;
}
///
/// Gets the visual line that contains the document line with the specified number.
/// If that line is outside the visible range, a new VisualLine for that document line is constructed.
///
public VisualLine GetOrConstructVisualLine(DocumentLine documentLine)
{
if (documentLine == null)
throw new ArgumentNullException("documentLine");
if (!this.Document.Lines.Contains(documentLine))
throw new InvalidOperationException("Line belongs to wrong document");
VerifyAccess();
VisualLine l = GetVisualLine(documentLine.LineNumber);
if (l == null) {
TextRunProperties globalTextRunProperties = CreateGlobalTextRunProperties();
VisualLineTextParagraphProperties paragraphProperties = CreateParagraphProperties(globalTextRunProperties);
while (heightTree.GetIsCollapsed(documentLine.LineNumber)) {
documentLine = documentLine.PreviousLine;
}
l = BuildVisualLine(documentLine,
globalTextRunProperties, paragraphProperties,
elementGenerators.ToArray(), lineTransformers.ToArray(),
lastAvailableSize);
allVisualLines.Add(l);
// update all visual top values (building the line might have changed visual top of other lines due to word wrapping)
foreach (var line in allVisualLines) {
line.VisualTop = heightTree.GetVisualPosition(line.FirstDocumentLine);
}
}
return l;
}
#endregion
#region Visual Lines (fields and properties)
List allVisualLines = new List();
ReadOnlyCollection visibleVisualLines;
double clippedPixelsOnTop;
List newVisualLines;
///
/// Gets the currently visible visual lines.
///
///
/// Gets thrown if there are invalid visual lines when this property is accessed.
/// You can use the property to check for this case,
/// or use the method to force creating the visual lines
/// when they are invalid.
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations")]
public ReadOnlyCollection VisualLines {
get {
if (visibleVisualLines == null)
throw new VisualLinesInvalidException();
return visibleVisualLines;
}
}
///
/// Gets whether the visual lines are valid.
/// Will return false after a call to Redraw().
/// Accessing the visual lines property will cause a
/// if this property is false.
///
public bool VisualLinesValid {
get { return visibleVisualLines != null; }
}
///
/// Occurs when the TextView is about to be measured and will regenerate its visual lines.
/// This event may be used to mark visual lines as invalid that would otherwise be reused.
///
public event EventHandler VisualLineConstructionStarting;
///
/// Occurs when the TextView was measured and changed its visual lines.
///
public event EventHandler VisualLinesChanged;
///
/// If the visual lines are invalid, creates new visual lines for the visible part
/// of the document.
/// If all visual lines are valid, this method does nothing.
///
/// The visual line build process is already running.
/// It is not allowed to call this method during the construction of a visual line.
public void EnsureVisualLines()
{
Dispatcher.VerifyAccess();
if (inMeasure)
throw new InvalidOperationException("The visual line build process is already running! Cannot EnsureVisualLines() during Measure!");
if (!VisualLinesValid) {
// increase priority for re-measure
InvalidateMeasure(DispatcherPriority.Normal);
// force immediate re-measure
UpdateLayout();
}
// Sometimes we still have invalid lines after UpdateLayout - work around the problem
// by calling MeasureOverride directly.
if (!VisualLinesValid) {
Debug.WriteLine("UpdateLayout() failed in EnsureVisualLines");
MeasureOverride(lastAvailableSize);
}
if (!VisualLinesValid)
throw new VisualLinesInvalidException("Internal error: visual lines invalid after EnsureVisualLines call");
}
#endregion
#region Measure
///
/// Additonal amount that allows horizontal scrolling past the end of the longest line.
/// This is necessary to ensure the caret always is visible, even when it is at the end of the longest line.
///
const double AdditionalHorizontalScrollAmount = 3;
Size lastAvailableSize;
bool inMeasure;
///
protected override Size MeasureOverride(Size availableSize)
{
// We don't support infinite available width, so we'll limit it to 32000 pixels.
if (availableSize.Width > 32000)
availableSize.Width = 32000;
if (!canHorizontallyScroll && !availableSize.Width.IsClose(lastAvailableSize.Width))
ClearVisualLines();
lastAvailableSize = availableSize;
foreach (UIElement layer in layers) {
layer.Measure(availableSize);
}
MeasureInlineObjects();
InvalidateVisual(); // = InvalidateArrange+InvalidateRender
double maxWidth;
if (document == null) {
// no document -> create empty list of lines
allVisualLines = new List();
visibleVisualLines = allVisualLines.AsReadOnly();
maxWidth = 0;
} else {
inMeasure = true;
try {
maxWidth = CreateAndMeasureVisualLines(availableSize);
} finally {
inMeasure = false;
}
}
// remove inline objects only at the end, so that inline objects that were re-used are not removed from the editor
RemoveInlineObjectsNow();
maxWidth += AdditionalHorizontalScrollAmount;
double heightTreeHeight = this.DocumentHeight;
TextEditorOptions options = this.Options;
if (options.AllowScrollBelowDocument) {
if (!double.IsInfinity(scrollViewport.Height)) {
heightTreeHeight = Math.Max(heightTreeHeight, Math.Min(heightTreeHeight - 50, scrollOffset.Y) + scrollViewport.Height);
}
}
textLayer.SetVisualLines(visibleVisualLines);
SetScrollData(availableSize,
new Size(maxWidth, heightTreeHeight),
scrollOffset);
if (VisualLinesChanged != null)
VisualLinesChanged(this, EventArgs.Empty);
return new Size(Math.Min(availableSize.Width, maxWidth), Math.Min(availableSize.Height, heightTreeHeight));
}
///
/// Build all VisualLines in the visible range.
///
/// Width the longest line
double CreateAndMeasureVisualLines(Size availableSize)
{
TextRunProperties globalTextRunProperties = CreateGlobalTextRunProperties();
VisualLineTextParagraphProperties paragraphProperties = CreateParagraphProperties(globalTextRunProperties);
Debug.WriteLine("Measure availableSize=" + availableSize + ", scrollOffset=" + scrollOffset);
var firstLineInView = heightTree.GetLineByVisualPosition(scrollOffset.Y);
// number of pixels clipped from the first visual line(s)
clippedPixelsOnTop = scrollOffset.Y - heightTree.GetVisualPosition(firstLineInView);
// clippedPixelsOnTop should be >= 0, except for floating point inaccurracy.
Debug.Assert(clippedPixelsOnTop >= -ExtensionMethods.Epsilon);
newVisualLines = new List();
if (VisualLineConstructionStarting != null)
VisualLineConstructionStarting(this, new VisualLineConstructionStartEventArgs(firstLineInView));
var elementGeneratorsArray = elementGenerators.ToArray();
var lineTransformersArray = lineTransformers.ToArray();
var nextLine = firstLineInView;
double maxWidth = 0;
double yPos = -clippedPixelsOnTop;
while (yPos < availableSize.Height && nextLine != null) {
VisualLine visualLine = GetVisualLine(nextLine.LineNumber);
if (visualLine == null) {
visualLine = BuildVisualLine(nextLine,
globalTextRunProperties, paragraphProperties,
elementGeneratorsArray, lineTransformersArray,
availableSize);
}
visualLine.VisualTop = scrollOffset.Y + yPos;
nextLine = visualLine.LastDocumentLine.NextLine;
yPos += visualLine.Height;
foreach (TextLine textLine in visualLine.TextLines) {
if (textLine.WidthIncludingTrailingWhitespace > maxWidth)
maxWidth = textLine.WidthIncludingTrailingWhitespace;
}
newVisualLines.Add(visualLine);
}
foreach (VisualLine line in allVisualLines) {
Debug.Assert(line.IsDisposed == false);
if (!newVisualLines.Contains(line))
DisposeVisualLine(line);
}
allVisualLines = newVisualLines;
// visibleVisualLines = readonly copy of visual lines
visibleVisualLines = new ReadOnlyCollection(newVisualLines.ToArray());
newVisualLines = null;
if (allVisualLines.Any(line => line.IsDisposed)) {
throw new InvalidOperationException("A visual line was disposed even though it is still in use.\n" +
"This can happen when Redraw() is called during measure for lines " +
"that are already constructed.");
}
return maxWidth;
}
#endregion
#region BuildVisualLine
TextFormatter formatter;
internal TextViewCachedElements cachedElements;
TextRunProperties CreateGlobalTextRunProperties()
{
var p = new GlobalTextRunProperties();
p.typeface = this.CreateTypeface();
p.fontRenderingEmSize = FontSize;
p.foregroundBrush = (Brush)GetValue(Control.ForegroundProperty);
ExtensionMethods.CheckIsFrozen(p.foregroundBrush);
p.cultureInfo = CultureInfo.CurrentCulture;
return p;
}
VisualLineTextParagraphProperties CreateParagraphProperties(TextRunProperties defaultTextRunProperties)
{
return new VisualLineTextParagraphProperties {
defaultTextRunProperties = defaultTextRunProperties,
textWrapping = canHorizontallyScroll ? TextWrapping.NoWrap : TextWrapping.Wrap,
tabSize = Options.IndentationSize * WideSpaceWidth
};
}
VisualLine BuildVisualLine(DocumentLine documentLine,
TextRunProperties globalTextRunProperties,
VisualLineTextParagraphProperties paragraphProperties,
VisualLineElementGenerator[] elementGeneratorsArray,
IVisualLineTransformer[] lineTransformersArray,
Size availableSize)
{
if (heightTree.GetIsCollapsed(documentLine.LineNumber))
throw new InvalidOperationException("Trying to build visual line from collapsed line");
//Debug.WriteLine("Building line " + documentLine.LineNumber);
VisualLine visualLine = new VisualLine(this, documentLine);
VisualLineTextSource textSource = new VisualLineTextSource(visualLine) {
Document = document,
GlobalTextRunProperties = globalTextRunProperties,
TextView = this
};
visualLine.ConstructVisualElements(textSource, elementGeneratorsArray);
if (visualLine.FirstDocumentLine != visualLine.LastDocumentLine) {
// Check whether the lines are collapsed correctly:
double firstLinePos = heightTree.GetVisualPosition(visualLine.FirstDocumentLine.NextLine);
double lastLinePos = heightTree.GetVisualPosition(visualLine.LastDocumentLine.NextLine ?? visualLine.LastDocumentLine);
if (!firstLinePos.IsClose(lastLinePos)) {
for (int i = visualLine.FirstDocumentLine.LineNumber + 1; i <= visualLine.LastDocumentLine.LineNumber; i++) {
if (!heightTree.GetIsCollapsed(i))
throw new InvalidOperationException("Line " + i + " was skipped by a VisualLineElementGenerator, but it is not collapsed.");
}
throw new InvalidOperationException("All lines collapsed but visual pos different - height tree inconsistency?");
}
}
visualLine.RunTransformers(textSource, lineTransformersArray);
// now construct textLines:
int textOffset = 0;
TextLineBreak lastLineBreak = null;
var textLines = new List();
paragraphProperties.indent = 0;
paragraphProperties.firstLineInParagraph = true;
while (textOffset <= visualLine.VisualLengthWithEndOfLineMarker) {
TextLine textLine = formatter.FormatLine(
textSource,
textOffset,
availableSize.Width,
paragraphProperties,
lastLineBreak
);
textLines.Add(textLine);
textOffset += textLine.Length;
// exit loop so that we don't do the indentation calculation if there's only a single line
if (textOffset >= visualLine.VisualLengthWithEndOfLineMarker)
break;
if (paragraphProperties.firstLineInParagraph) {
paragraphProperties.firstLineInParagraph = false;
TextEditorOptions options = this.Options;
double indentation = 0;
if (options.InheritWordWrapIndentation) {
// determine indentation for next line:
int indentVisualColumn = GetIndentationVisualColumn(visualLine);
if (indentVisualColumn > 0 && indentVisualColumn < textOffset) {
indentation = textLine.GetDistanceFromCharacterHit(new CharacterHit(indentVisualColumn, 0));
}
}
indentation += options.WordWrapIndentation;
// apply the calculated indentation unless it's more than half of the text editor size:
if (indentation > 0 && indentation * 2 < availableSize.Width)
paragraphProperties.indent = indentation;
}
lastLineBreak = textLine.GetTextLineBreak();
}
visualLine.SetTextLines(textLines);
heightTree.SetHeight(visualLine.FirstDocumentLine, visualLine.Height);
return visualLine;
}
static int GetIndentationVisualColumn(VisualLine visualLine)
{
if (visualLine.Elements.Count == 0)
return 0;
int column = 0;
int elementIndex = 0;
VisualLineElement element = visualLine.Elements[elementIndex];
while (element.IsWhitespace(column)) {
column++;
if (column == element.VisualColumn + element.VisualLength) {
elementIndex++;
if (elementIndex == visualLine.Elements.Count)
break;
element = visualLine.Elements[elementIndex];
}
}
return column;
}
#endregion
#region Arrange
///
/// Arrange implementation.
///
protected override Size ArrangeOverride(Size finalSize)
{
EnsureVisualLines();
foreach (UIElement layer in layers) {
layer.Arrange(new Rect(new Point(0, 0), finalSize));
}
if (document == null || allVisualLines.Count == 0)
return finalSize;
// validate scroll position
Vector newScrollOffset = scrollOffset;
if (scrollOffset.X + finalSize.Width > scrollExtent.Width) {
newScrollOffset.X = Math.Max(0, scrollExtent.Width - finalSize.Width);
}
if (scrollOffset.Y + finalSize.Height > scrollExtent.Height) {
newScrollOffset.Y = Math.Max(0, scrollExtent.Height - finalSize.Height);
}
if (SetScrollData(scrollViewport, scrollExtent, newScrollOffset))
InvalidateMeasure(DispatcherPriority.Normal);
//Debug.WriteLine("Arrange finalSize=" + finalSize + ", scrollOffset=" + scrollOffset);
// double maxWidth = 0;
if (visibleVisualLines != null) {
Point pos = new Point(-scrollOffset.X, -clippedPixelsOnTop);
foreach (VisualLine visualLine in visibleVisualLines) {
int offset = 0;
foreach (TextLine textLine in visualLine.TextLines) {
foreach (var span in textLine.GetTextRunSpans()) {
InlineObjectRun inline = span.Value as InlineObjectRun;
if (inline != null && inline.VisualLine != null) {
Debug.Assert(inlineObjects.Contains(inline));
double distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(offset, 0));
inline.Element.Arrange(new Rect(new Point(pos.X + distance, pos.Y), inline.Element.DesiredSize));
}
offset += span.Length;
}
pos.Y += textLine.Height;
}
}
}
InvalidateCursorIfMouseWithinTextView();
return finalSize;
}
#endregion
#region Render
readonly ObserveAddRemoveCollection backgroundRenderers;
///
/// Gets the list of background renderers.
///
public IList BackgroundRenderers {
get { return backgroundRenderers; }
}
void BackgroundRenderer_Added(IBackgroundRenderer renderer)
{
ConnectToTextView(renderer);
InvalidateLayer(renderer.Layer);
}
void BackgroundRenderer_Removed(IBackgroundRenderer renderer)
{
DisconnectFromTextView(renderer);
InvalidateLayer(renderer.Layer);
}
///
protected override void OnRender(DrawingContext drawingContext)
{
RenderBackground(drawingContext, KnownLayer.Background);
foreach (var line in visibleVisualLines) {
Brush currentBrush = null;
int startVC = 0;
int length = 0;
foreach (var element in line.Elements) {
if (currentBrush == null || !currentBrush.Equals(element.BackgroundBrush)) {
if (currentBrush != null) {
BackgroundGeometryBuilder builder = new BackgroundGeometryBuilder();
builder.AlignToWholePixels = true;
builder.CornerRadius = 3;
foreach (var rect in BackgroundGeometryBuilder.GetRectsFromVisualSegment(this, line, startVC, startVC + length))
builder.AddRectangle(this, rect);
Geometry geometry = builder.CreateGeometry();
if (geometry != null) {
drawingContext.DrawGeometry(currentBrush, null, geometry);
}
}
startVC = element.VisualColumn;
length = element.DocumentLength;
currentBrush = element.BackgroundBrush;
} else {
length += element.VisualLength;
}
}
if (currentBrush != null) {
BackgroundGeometryBuilder builder = new BackgroundGeometryBuilder();
builder.AlignToWholePixels = true;
builder.CornerRadius = 3;
foreach (var rect in BackgroundGeometryBuilder.GetRectsFromVisualSegment(this, line, startVC, startVC + length))
builder.AddRectangle(this, rect);
Geometry geometry = builder.CreateGeometry();
if (geometry != null) {
drawingContext.DrawGeometry(currentBrush, null, geometry);
}
}
}
}
internal void RenderBackground(DrawingContext drawingContext, KnownLayer layer)
{
foreach (IBackgroundRenderer bg in backgroundRenderers) {
if (bg.Layer == layer) {
bg.Draw(this, drawingContext);
}
}
}
internal void ArrangeTextLayer(IList visuals)
{
Point pos = new Point(-scrollOffset.X, -clippedPixelsOnTop);
foreach (VisualLineDrawingVisual visual in visuals) {
TranslateTransform t = visual.Transform as TranslateTransform;
if (t == null || t.X != pos.X || t.Y != pos.Y) {
visual.Transform = new TranslateTransform(pos.X, pos.Y);
visual.Transform.Freeze();
}
pos.Y += visual.Height;
}
}
#endregion
#region IScrollInfo implementation
///
/// Size of the document, in pixels.
///
Size scrollExtent;
///
/// Offset of the scroll position.
///
Vector scrollOffset;
///
/// Size of the viewport.
///
Size scrollViewport;
void ClearScrollData()
{
SetScrollData(new Size(), new Size(), new Vector());
}
bool SetScrollData(Size viewport, Size extent, Vector offset)
{
if (!(viewport.IsClose(this.scrollViewport)
&& extent.IsClose(this.scrollExtent)
&& offset.IsClose(this.scrollOffset)))
{
this.scrollViewport = viewport;
this.scrollExtent = extent;
SetScrollOffset(offset);
this.OnScrollChange();
return true;
}
return false;
}
void OnScrollChange()
{
ScrollViewer scrollOwner = ((IScrollInfo)this).ScrollOwner;
if (scrollOwner != null) {
scrollOwner.InvalidateScrollInfo();
}
}
bool canVerticallyScroll;
bool IScrollInfo.CanVerticallyScroll {
get { return canVerticallyScroll; }
set {
if (canVerticallyScroll != value) {
canVerticallyScroll = value;
InvalidateMeasure(DispatcherPriority.Normal);
}
}
}
bool canHorizontallyScroll;
bool IScrollInfo.CanHorizontallyScroll {
get { return canHorizontallyScroll; }
set {
if (canHorizontallyScroll != value) {
canHorizontallyScroll = value;
ClearVisualLines();
InvalidateMeasure(DispatcherPriority.Normal);
}
}
}
double IScrollInfo.ExtentWidth {
get { return scrollExtent.Width; }
}
double IScrollInfo.ExtentHeight {
get { return scrollExtent.Height; }
}
double IScrollInfo.ViewportWidth {
get { return scrollViewport.Width; }
}
double IScrollInfo.ViewportHeight {
get { return scrollViewport.Height; }
}
///
/// Gets the horizontal scroll offset.
///
public double HorizontalOffset {
get { return scrollOffset.X; }
}
///
/// Gets the vertical scroll offset.
///
public double VerticalOffset {
get { return scrollOffset.Y; }
}
///
/// Gets the scroll offset;
///
public Vector ScrollOffset {
get { return scrollOffset; }
}
///
/// Occurs when the scroll offset has changed.
///
public event EventHandler ScrollOffsetChanged;
void SetScrollOffset(Vector vector)
{
if (!canHorizontallyScroll)
vector.X = 0;
if (!canVerticallyScroll)
vector.Y = 0;
if (!scrollOffset.IsClose(vector)) {
scrollOffset = vector;
if (ScrollOffsetChanged != null)
ScrollOffsetChanged(this, EventArgs.Empty);
}
}
ScrollViewer IScrollInfo.ScrollOwner { get; set; }
void IScrollInfo.LineUp()
{
((IScrollInfo)this).SetVerticalOffset(scrollOffset.Y - DefaultLineHeight);
}
void IScrollInfo.LineDown()
{
((IScrollInfo)this).SetVerticalOffset(scrollOffset.Y + DefaultLineHeight);
}
void IScrollInfo.LineLeft()
{
((IScrollInfo)this).SetHorizontalOffset(scrollOffset.X - WideSpaceWidth);
}
void IScrollInfo.LineRight()
{
((IScrollInfo)this).SetHorizontalOffset(scrollOffset.X + WideSpaceWidth);
}
void IScrollInfo.PageUp()
{
((IScrollInfo)this).SetVerticalOffset(scrollOffset.Y - scrollViewport.Height);
}
void IScrollInfo.PageDown()
{
((IScrollInfo)this).SetVerticalOffset(scrollOffset.Y + scrollViewport.Height);
}
void IScrollInfo.PageLeft()
{
((IScrollInfo)this).SetHorizontalOffset(scrollOffset.X - scrollViewport.Width);
}
void IScrollInfo.PageRight()
{
((IScrollInfo)this).SetHorizontalOffset(scrollOffset.X + scrollViewport.Width);
}
void IScrollInfo.MouseWheelUp()
{
((IScrollInfo)this).SetVerticalOffset(
scrollOffset.Y - (SystemParameters.WheelScrollLines * DefaultLineHeight));
OnScrollChange();
}
void IScrollInfo.MouseWheelDown()
{
((IScrollInfo)this).SetVerticalOffset(
scrollOffset.Y + (SystemParameters.WheelScrollLines * DefaultLineHeight));
OnScrollChange();
}
void IScrollInfo.MouseWheelLeft()
{
((IScrollInfo)this).SetHorizontalOffset(
scrollOffset.X - (SystemParameters.WheelScrollLines * WideSpaceWidth));
OnScrollChange();
}
void IScrollInfo.MouseWheelRight()
{
((IScrollInfo)this).SetHorizontalOffset(
scrollOffset.X + (SystemParameters.WheelScrollLines * WideSpaceWidth));
OnScrollChange();
}
bool defaultTextMetricsValid;
double wideSpaceWidth; // Width of an 'x'. Used as basis for the tab width, and for scrolling.
double defaultLineHeight; // Height of a line containing 'x'. Used for scrolling.
double defaultBaseline; // Baseline of a line containing 'x'. Used for TextTop/TextBottom calculation.
///
/// Gets the width of a 'wide space' (the space width used for calculating the tab size).
///
///
/// This is the width of an 'x' in the current font.
/// We do not measure the width of an actual space as that would lead to tiny tabs in
/// some proportional fonts.
/// For monospaced fonts, this property will return the expected value, as 'x' and ' ' have the same width.
///
public double WideSpaceWidth {
get {
CalculateDefaultTextMetrics();
return wideSpaceWidth;
}
}
///
/// Gets the default line height. This is the height of an empty line or a line containing regular text.
/// Lines that include formatted text or custom UI elements may have a different line height.
///
public double DefaultLineHeight {
get {
CalculateDefaultTextMetrics();
return defaultLineHeight;
}
}
///
/// Gets the default baseline position. This is the difference between
/// and for a line containing regular text.
/// Lines that include formatted text or custom UI elements may have a different baseline.
///
public double DefaultBaseline {
get {
CalculateDefaultTextMetrics();
return defaultBaseline;
}
}
void InvalidateDefaultTextMetrics()
{
defaultTextMetricsValid = false;
if (heightTree != null) {
// calculate immediately so that height tree gets updated
CalculateDefaultTextMetrics();
}
}
void CalculateDefaultTextMetrics()
{
if (defaultTextMetricsValid)
return;
defaultTextMetricsValid = true;
if (formatter != null) {
var textRunProperties = CreateGlobalTextRunProperties();
using (var line = formatter.FormatLine(
new SimpleTextSource("x", textRunProperties),
0, 32000,
new VisualLineTextParagraphProperties { defaultTextRunProperties = textRunProperties },
null))
{
wideSpaceWidth = Math.Max(1, line.WidthIncludingTrailingWhitespace);
defaultBaseline = Math.Max(1, line.Baseline);
defaultLineHeight = Math.Max(1, line.Height);
}
} else {
wideSpaceWidth = FontSize / 2;
defaultBaseline = FontSize;
defaultLineHeight = FontSize + 3;
}
// Update heightTree.DefaultLineHeight, if a document is loaded.
if (heightTree != null)
heightTree.DefaultLineHeight = defaultLineHeight;
}
static double ValidateVisualOffset(double offset)
{
if (double.IsNaN(offset))
throw new ArgumentException("offset must not be NaN");
if (offset < 0)
return 0;
else
return offset;
}
void IScrollInfo.SetHorizontalOffset(double offset)
{
offset = ValidateVisualOffset(offset);
if (!scrollOffset.X.IsClose(offset)) {
SetScrollOffset(new Vector(offset, scrollOffset.Y));
InvalidateVisual();
textLayer.InvalidateVisual();
}
}
void IScrollInfo.SetVerticalOffset(double offset)
{
offset = ValidateVisualOffset(offset);
if (!scrollOffset.Y.IsClose(offset)) {
SetScrollOffset(new Vector(scrollOffset.X, offset));
InvalidateMeasure(DispatcherPriority.Normal);
}
}
Rect IScrollInfo.MakeVisible(Visual visual, Rect rectangle)
{
if (rectangle.IsEmpty || visual == null || visual == this || !this.IsAncestorOf(visual)) {
return Rect.Empty;
}
// Convert rectangle into our coordinate space.
GeneralTransform childTransform = visual.TransformToAncestor(this);
rectangle = childTransform.TransformBounds(rectangle);
MakeVisible(Rect.Offset(rectangle, scrollOffset));
return rectangle;
}
///
/// Scrolls the text view so that the specified rectangle gets visible.
///
public void MakeVisible(Rect rectangle)
{
Rect visibleRectangle = new Rect(scrollOffset.X, scrollOffset.Y,
scrollViewport.Width, scrollViewport.Height);
Vector newScrollOffset = scrollOffset;
if (rectangle.Left < visibleRectangle.Left) {
if (rectangle.Right > visibleRectangle.Right) {
newScrollOffset.X = rectangle.Left + rectangle.Width / 2;
} else {
newScrollOffset.X = rectangle.Left;
}
} else if (rectangle.Right > visibleRectangle.Right) {
newScrollOffset.X = rectangle.Right - scrollViewport.Width;
}
if (rectangle.Top < visibleRectangle.Top) {
if (rectangle.Bottom > visibleRectangle.Bottom) {
newScrollOffset.Y = rectangle.Top + rectangle.Height / 2;
} else {
newScrollOffset.Y = rectangle.Top;
}
} else if (rectangle.Bottom > visibleRectangle.Bottom) {
newScrollOffset.Y = rectangle.Bottom - scrollViewport.Height;
}
newScrollOffset.X = ValidateVisualOffset(newScrollOffset.X);
newScrollOffset.Y = ValidateVisualOffset(newScrollOffset.Y);
if (!scrollOffset.IsClose(newScrollOffset)) {
SetScrollOffset(newScrollOffset);
this.OnScrollChange();
InvalidateMeasure(DispatcherPriority.Normal);
}
}
#endregion
#region Visual element mouse handling
///
protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
{
// accept clicks even where the text area draws no background
return new PointHitTestResult(this, hitTestParameters.HitPoint);
}
[ThreadStatic] static bool invalidCursor;
///
/// Updates the mouse cursor by calling , but with background priority.
///
public static void InvalidateCursor()
{
if (!invalidCursor) {
invalidCursor = true;
Dispatcher.CurrentDispatcher.BeginInvoke(
DispatcherPriority.Background, // fixes issue #288
new Action(
delegate {
invalidCursor = false;
Mouse.UpdateCursor();
}));
}
}
internal void InvalidateCursorIfMouseWithinTextView()
{
// Don't unnecessarily call Mouse.UpdateCursor() if the mouse is outside the text view.
// Unnecessary updates may cause the mouse pointer to flicker
// (e.g. if it is over a window border, it blinks between Resize and Normal)
if (this.IsMouseOver)
InvalidateCursor();
}
///
protected override void OnQueryCursor(QueryCursorEventArgs e)
{
VisualLineElement element = GetVisualLineElementFromPosition(e.GetPosition(this) + scrollOffset);
if (element != null) {
element.OnQueryCursor(e);
}
}
///
protected override void OnMouseDown(MouseButtonEventArgs e)
{
base.OnMouseDown(e);
if (!e.Handled) {
EnsureVisualLines();
VisualLineElement element = GetVisualLineElementFromPosition(e.GetPosition(this) + scrollOffset);
if (element != null) {
element.OnMouseDown(e);
}
}
}
///
protected override void OnMouseUp(MouseButtonEventArgs e)
{
base.OnMouseUp(e);
if (!e.Handled) {
EnsureVisualLines();
VisualLineElement element = GetVisualLineElementFromPosition(e.GetPosition(this) + scrollOffset);
if (element != null) {
element.OnMouseUp(e);
}
}
}
#endregion
#region Getting elements from Visual Position
///
/// Gets the visual line at the specified document position (relative to start of document).
/// Returns null if there is no visual line for the position (e.g. the position is outside the visible
/// text area).
///
public VisualLine GetVisualLineFromVisualTop(double visualTop)
{
// TODO: change this method to also work outside the visible range -
// required to make GetPosition work as expected!
EnsureVisualLines();
foreach (VisualLine vl in this.VisualLines) {
if (visualTop < vl.VisualTop)
continue;
if (visualTop < vl.VisualTop + vl.Height)
return vl;
}
return null;
}
///
/// Gets the visual top position (relative to start of document) from a document line number.
///
public double GetVisualTopByDocumentLine(int line)
{
VerifyAccess();
if (heightTree == null)
throw ThrowUtil.NoDocumentAssigned();
return heightTree.GetVisualPosition(heightTree.GetLineByNumber(line));
}
VisualLineElement GetVisualLineElementFromPosition(Point visualPosition)
{
VisualLine vl = GetVisualLineFromVisualTop(visualPosition.Y);
if (vl != null) {
int column = vl.GetVisualColumnFloor(visualPosition);
// Debug.WriteLine(vl.FirstDocumentLine.LineNumber + " vc " + column);
foreach (VisualLineElement element in vl.Elements) {
if (element.VisualColumn + element.VisualLength <= column)
continue;
return element;
}
}
return null;
}
#endregion
#region Visual Position <-> TextViewPosition
///
/// Gets the visual position from a text view position.
///
/// The text view position.
/// The mode how to retrieve the Y position.
/// The position in WPF device-independent pixels relative
/// to the top left corner of the document.
public Point GetVisualPosition(TextViewPosition position, VisualYPosition yPositionMode)
{
VerifyAccess();
if (this.Document == null)
throw ThrowUtil.NoDocumentAssigned();
DocumentLine documentLine = this.Document.GetLineByNumber(position.Line);
VisualLine visualLine = GetOrConstructVisualLine(documentLine);
int visualColumn = position.VisualColumn;
if (visualColumn < 0) {
int offset = documentLine.Offset + position.Column - 1;
visualColumn = visualLine.GetVisualColumn(offset - visualLine.FirstDocumentLine.Offset);
}
return visualLine.GetVisualPosition(visualColumn, position.IsAtEndOfLine, yPositionMode);
}
///
/// Gets the text view position from the specified visual position.
/// If the position is within a character, it is rounded to the next character boundary.
///
/// The position in WPF device-independent pixels relative
/// to the top left corner of the document.
/// The logical position, or null if the position is outside the document.
public TextViewPosition? GetPosition(Point visualPosition)
{
VerifyAccess();
if (this.Document == null)
throw ThrowUtil.NoDocumentAssigned();
VisualLine line = GetVisualLineFromVisualTop(visualPosition.Y);
if (line == null)
return null;
return line.GetTextViewPosition(visualPosition, Options.EnableVirtualSpace);
}
///
/// Gets the text view position from the specified visual position.
/// If the position is inside a character, the position in front of the character is returned.
///
/// The position in WPF device-independent pixels relative
/// to the top left corner of the document.
/// The logical position, or null if the position is outside the document.
public TextViewPosition? GetPositionFloor(Point visualPosition)
{
VerifyAccess();
if (this.Document == null)
throw ThrowUtil.NoDocumentAssigned();
VisualLine line = GetVisualLineFromVisualTop(visualPosition.Y);
if (line == null)
return null;
return line.GetTextViewPositionFloor(visualPosition, Options.EnableVirtualSpace);
}
#endregion
#region Service Provider
readonly ServiceContainer services = new ServiceContainer();
///
/// Gets a service container used to associate services with the text view.
///
///
/// This container does not provide document services -
/// use TextView.GetService() instead of TextView.Services.GetService() to ensure
/// that document services can be found as well.
///
public ServiceContainer Services {
get { return services; }
}
///
/// Retrieves a service from the text view.
/// If the service is not found in the container,
/// this method will also look for it in the current document's service provider.
///
public virtual object GetService(Type serviceType)
{
object instance = services.GetService(serviceType);
if (instance == null && document != null) {
instance = document.ServiceProvider.GetService(serviceType);
}
return instance;
}
void ConnectToTextView(object obj)
{
ITextViewConnect c = obj as ITextViewConnect;
if (c != null)
c.AddToTextView(this);
}
void DisconnectFromTextView(object obj)
{
ITextViewConnect c = obj as ITextViewConnect;
if (c != null)
c.RemoveFromTextView(this);
}
#endregion
#region MouseHover
///
/// The PreviewMouseHover event.
///
public static readonly RoutedEvent PreviewMouseHoverEvent =
EventManager.RegisterRoutedEvent("PreviewMouseHover", RoutingStrategy.Tunnel,
typeof(MouseEventHandler), typeof(TextView));
///
/// The MouseHover event.
///
public static readonly RoutedEvent MouseHoverEvent =
EventManager.RegisterRoutedEvent("MouseHover", RoutingStrategy.Bubble,
typeof(MouseEventHandler), typeof(TextView));
///
/// The PreviewMouseHoverStopped event.
///
public static readonly RoutedEvent PreviewMouseHoverStoppedEvent =
EventManager.RegisterRoutedEvent("PreviewMouseHoverStopped", RoutingStrategy.Tunnel,
typeof(MouseEventHandler), typeof(TextView));
///
/// The MouseHoverStopped event.
///
public static readonly RoutedEvent MouseHoverStoppedEvent =
EventManager.RegisterRoutedEvent("MouseHoverStopped", RoutingStrategy.Bubble,
typeof(MouseEventHandler), typeof(TextView));
///
/// Occurs when the mouse has hovered over a fixed location for some time.
///
public event MouseEventHandler PreviewMouseHover {
add { AddHandler(PreviewMouseHoverEvent, value); }
remove { RemoveHandler(PreviewMouseHoverEvent, value); }
}
///
/// Occurs when the mouse has hovered over a fixed location for some time.
///
public event MouseEventHandler MouseHover {
add { AddHandler(MouseHoverEvent, value); }
remove { RemoveHandler(MouseHoverEvent, value); }
}
///
/// Occurs when the mouse had previously hovered but now started moving again.
///
public event MouseEventHandler PreviewMouseHoverStopped {
add { AddHandler(PreviewMouseHoverStoppedEvent, value); }
remove { RemoveHandler(PreviewMouseHoverStoppedEvent, value); }
}
///
/// Occurs when the mouse had previously hovered but now started moving again.
///
public event MouseEventHandler MouseHoverStopped {
add { AddHandler(MouseHoverStoppedEvent, value); }
remove { RemoveHandler(MouseHoverStoppedEvent, value); }
}
MouseHoverLogic hoverLogic;
void RaiseHoverEventPair(MouseEventArgs e, RoutedEvent tunnelingEvent, RoutedEvent bubblingEvent)
{
var mouseDevice = e.MouseDevice;
var stylusDevice = e.StylusDevice;
int inputTime = Environment.TickCount;
var args1 = new MouseEventArgs(mouseDevice, inputTime, stylusDevice) {
RoutedEvent = tunnelingEvent,
Source = this
};
RaiseEvent(args1);
var args2 = new MouseEventArgs(mouseDevice, inputTime, stylusDevice) {
RoutedEvent = bubblingEvent,
Source = this,
Handled = args1.Handled
};
RaiseEvent(args2);
}
#endregion
///
/// Collapses lines for the purpose of scrolling. s marked as collapsed will be hidden
/// and not used to start the generation of a .
///
///
/// This method is meant for s that cause s to span
/// multiple s. Do not call it without providing a corresponding
/// .
/// If you want to create collapsible text sections, see .
///
/// Note that if you want a VisualLineElement to span from line N to line M, then you need to collapse only the lines
/// N+1 to M. Do not collapse line N itself.
///
/// When you no longer need the section to be collapsed, call on the
/// returned from this method.
///
public CollapsedLineSection CollapseLines(DocumentLine start, DocumentLine end)
{
VerifyAccess();
if (heightTree == null)
throw ThrowUtil.NoDocumentAssigned();
return heightTree.CollapseText(start, end);
}
///
/// Gets the height of the document.
///
public double DocumentHeight {
get {
// return 0 if there is no document = no heightTree
return heightTree != null ? heightTree.TotalHeight : 0;
}
}
///
/// Gets the document line at the specified visual position.
///
public DocumentLine GetDocumentLineByVisualTop(double visualTop)
{
VerifyAccess();
if (heightTree == null)
throw ThrowUtil.NoDocumentAssigned();
return heightTree.GetLineByVisualPosition(visualTop);
}
///
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (TextFormatterFactory.PropertyChangeAffectsTextFormatter(e.Property)) {
// first, create the new text formatter:
RecreateTextFormatter();
// changing text formatter requires recreating the cached elements
RecreateCachedElements();
// and we need to re-measure the font metrics:
InvalidateDefaultTextMetrics();
} else if (e.Property == Control.ForegroundProperty
|| e.Property == TextView.NonPrintableCharacterBrushProperty
|| e.Property == TextView.LinkTextBackgroundBrushProperty
|| e.Property == TextView.LinkTextForegroundBrushProperty)
{
// changing brushes requires recreating the cached elements
RecreateCachedElements();
Redraw();
}
if (e.Property == Control.FontFamilyProperty
|| e.Property == Control.FontSizeProperty
|| e.Property == Control.FontStretchProperty
|| e.Property == Control.FontStyleProperty
|| e.Property == Control.FontWeightProperty)
{
// changing font properties requires recreating cached elements
RecreateCachedElements();
// and we need to re-measure the font metrics:
InvalidateDefaultTextMetrics();
Redraw();
}
if (e.Property == ColumnRulerPenProperty) {
columnRulerRenderer.SetRuler(this.Options.ColumnRulerPosition, this.ColumnRulerPen);
}
if (e.Property == CurrentLineBorderProperty) {
currentLineHighlighRenderer.BorderPen = this.CurrentLineBorder;
}
if (e.Property == CurrentLineBackgroundProperty) {
currentLineHighlighRenderer.BackgroundBrush = this.CurrentLineBackground;
}
}
///
/// The pen used to draw the column ruler.
///
///
public static readonly DependencyProperty ColumnRulerPenProperty =
DependencyProperty.Register("ColumnRulerBrush", typeof(Pen), typeof(TextView),
new FrameworkPropertyMetadata(CreateFrozenPen(Brushes.LightGray)));
static Pen CreateFrozenPen(SolidColorBrush brush)
{
Pen pen = new Pen(brush, 1);
pen.Freeze();
return pen;
}
///
/// Gets/Sets the pen used to draw the column ruler.
///
///
public Pen ColumnRulerPen {
get { return (Pen)GetValue(ColumnRulerPenProperty); }
set { SetValue(ColumnRulerPenProperty, value); }
}
///
/// The property.
///
public static readonly DependencyProperty CurrentLineBackgroundProperty =
DependencyProperty.Register("CurrentLineBackground", typeof(Brush), typeof(TextView));
///
/// Gets/Sets the background brush used by current line highlighter.
///
public Brush CurrentLineBackground {
get { return (Brush)GetValue(CurrentLineBackgroundProperty); }
set { SetValue(CurrentLineBackgroundProperty, value); }
}
///
/// The property.
///
public static readonly DependencyProperty CurrentLineBorderProperty =
DependencyProperty.Register("CurrentLineBorder", typeof(Pen), typeof(TextView));
///
/// Gets/Sets the background brush used for the current line.
///
public Pen CurrentLineBorder {
get { return (Pen)GetValue(CurrentLineBorderProperty); }
set { SetValue(CurrentLineBorderProperty, value); }
}
///
/// Gets/Sets highlighted line number.
///
public int HighlightedLine {
get { return this.currentLineHighlighRenderer.Line; }
set { this.currentLineHighlighRenderer.Line = value; }
}
}
}