2004 lines
67 KiB
C#
2004 lines
67 KiB
C#
// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
|
|
// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
|
|
|
|
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.AvalonEdit.Document;
|
|
using ICSharpCode.AvalonEdit.Editing;
|
|
using ICSharpCode.AvalonEdit.Utils;
|
|
|
|
namespace ICSharpCode.AvalonEdit.Rendering
|
|
{
|
|
/// <summary>
|
|
/// A virtualizing panel producing+showing <see cref="VisualLine"/>s for a <see cref="TextDocument"/>.
|
|
///
|
|
/// 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.
|
|
/// </summary>
|
|
[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;
|
|
|
|
/// <summary>
|
|
/// Creates a new TextView instance.
|
|
/// </summary>
|
|
public TextView()
|
|
{
|
|
services.AddService(typeof(TextView), this);
|
|
textLayer = new TextLayer(this);
|
|
elementGenerators = new ObserveAddRemoveCollection<VisualLineElementGenerator>(ElementGenerator_Added, ElementGenerator_Removed);
|
|
lineTransformers = new ObserveAddRemoveCollection<IVisualLineTransformer>(LineTransformer_Added, LineTransformer_Removed);
|
|
backgroundRenderers = new ObserveAddRemoveCollection<IBackgroundRenderer>(BackgroundRenderer_Added, BackgroundRenderer_Removed);
|
|
columnRulerRenderer = new ColumnRulerRenderer(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
|
|
/// <summary>
|
|
/// Document property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty DocumentProperty =
|
|
DependencyProperty.Register("Document", typeof(TextDocument), typeof(TextView),
|
|
new FrameworkPropertyMetadata(OnDocumentChanged));
|
|
|
|
TextDocument document;
|
|
HeightTree heightTree;
|
|
|
|
/// <summary>
|
|
/// Gets/Sets the document displayed by the text editor.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Occurs when the document property has changed.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Recreates the text formatter that is used internally
|
|
/// by calling <see cref="TextFormatterFactory.Create"/>.
|
|
/// </summary>
|
|
void RecreateTextFormatter()
|
|
{
|
|
if (formatter != null) {
|
|
formatter.Dispose();
|
|
formatter = TextFormatterFactory.Create(this);
|
|
Redraw();
|
|
}
|
|
}
|
|
|
|
void RecreateCachedElements()
|
|
{
|
|
if (cachedElements != null) {
|
|
cachedElements.Dispose();
|
|
cachedElements = new TextViewCachedElements();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc cref="IWeakEventListener.ReceiveWeakEvent"/>
|
|
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
|
|
/// <summary>
|
|
/// Options property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty OptionsProperty =
|
|
DependencyProperty.Register("Options", typeof(TextEditorOptions), typeof(TextView),
|
|
new FrameworkPropertyMetadata(OnOptionsChanged));
|
|
|
|
/// <summary>
|
|
/// Gets/Sets the options used by the text editor.
|
|
/// </summary>
|
|
public TextEditorOptions Options {
|
|
get { return (TextEditorOptions)GetValue(OptionsProperty); }
|
|
set { SetValue(OptionsProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Occurs when a text editor option has changed.
|
|
/// </summary>
|
|
public event PropertyChangedEventHandler OptionChanged;
|
|
|
|
/// <summary>
|
|
/// Raises the <see cref="OptionChanged"/> event.
|
|
/// </summary>
|
|
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<VisualLineElementGenerator> elementGenerators;
|
|
|
|
/// <summary>
|
|
/// Gets a collection where element generators can be registered.
|
|
/// </summary>
|
|
public IList<VisualLineElementGenerator> ElementGenerators {
|
|
get { return elementGenerators; }
|
|
}
|
|
|
|
void ElementGenerator_Added(VisualLineElementGenerator generator)
|
|
{
|
|
ConnectToTextView(generator);
|
|
Redraw();
|
|
}
|
|
|
|
void ElementGenerator_Removed(VisualLineElementGenerator generator)
|
|
{
|
|
DisconnectFromTextView(generator);
|
|
Redraw();
|
|
}
|
|
|
|
readonly ObserveAddRemoveCollection<IVisualLineTransformer> lineTransformers;
|
|
|
|
/// <summary>
|
|
/// Gets a collection where line transformers can be registered.
|
|
/// </summary>
|
|
public IList<IVisualLineTransformer> 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<T>(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;
|
|
|
|
/// <summary>
|
|
/// Gets the list of layers displayed in the text view.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inserts a new layer at a position specified relative to an existing layer.
|
|
/// </summary>
|
|
/// <param name="layer">The new layer to insert.</param>
|
|
/// <param name="referencedLayer">The existing layer</param>
|
|
/// <param name="position">Specifies whether the layer is inserted above,below, or replaces the referenced layer</param>
|
|
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);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override int VisualChildrenCount {
|
|
get { return layers.Count + inlineObjects.Count; }
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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];
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override System.Collections.IEnumerator LogicalChildren {
|
|
get {
|
|
return inlineObjects.Select(io => io.Element).Concat(layers.Cast<UIElement>()).GetEnumerator();
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Inline object handling
|
|
List<InlineObjectRun> inlineObjects = new List<InlineObjectRun>();
|
|
|
|
/// <summary>
|
|
/// Adds a new inline object.
|
|
/// </summary>
|
|
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<VisualLine> visualLinesWithOutstandingInlineObjects = new List<VisualLine>();
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove the inline objects that were marked for removal.
|
|
/// </summary>
|
|
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
|
|
/// <summary>
|
|
/// NonPrintableCharacterBrush dependency property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty NonPrintableCharacterBrushProperty =
|
|
DependencyProperty.Register("NonPrintableCharacterBrush", typeof(Brush), typeof(TextView),
|
|
new FrameworkPropertyMetadata(Brushes.LightGray));
|
|
|
|
/// <summary>
|
|
/// Gets/sets the Brush used for displaying non-printable characters.
|
|
/// </summary>
|
|
public Brush NonPrintableCharacterBrush {
|
|
get { return (Brush)GetValue(NonPrintableCharacterBrushProperty); }
|
|
set { SetValue(NonPrintableCharacterBrushProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// LinkTextForegroundBrush dependency property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty LinkTextForegroundBrushProperty =
|
|
DependencyProperty.Register("LinkTextForegroundBrush", typeof(Brush), typeof(TextView),
|
|
new FrameworkPropertyMetadata(Brushes.Blue));
|
|
|
|
/// <summary>
|
|
/// Gets/sets the Brush used for displaying link texts.
|
|
/// </summary>
|
|
public Brush LinkTextForegroundBrush {
|
|
get { return (Brush)GetValue(LinkTextForegroundBrushProperty); }
|
|
set { SetValue(LinkTextForegroundBrushProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// LinkTextBackgroundBrush dependency property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty LinkTextBackgroundBrushProperty =
|
|
DependencyProperty.Register("LinkTextBackgroundBrush", typeof(Brush), typeof(TextView),
|
|
new FrameworkPropertyMetadata(Brushes.Transparent));
|
|
|
|
/// <summary>
|
|
/// Gets/sets the Brush used for the background of link texts.
|
|
/// </summary>
|
|
public Brush LinkTextBackgroundBrush {
|
|
get { return (Brush)GetValue(LinkTextBackgroundBrushProperty); }
|
|
set { SetValue(LinkTextBackgroundBrushProperty, value); }
|
|
}
|
|
#endregion
|
|
|
|
#region Redraw methods / VisualLine invalidation
|
|
/// <summary>
|
|
/// Causes the text editor to regenerate all visual lines.
|
|
/// </summary>
|
|
public void Redraw()
|
|
{
|
|
Redraw(DispatcherPriority.Normal);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Causes the text editor to regenerate all visual lines.
|
|
/// </summary>
|
|
public void Redraw(DispatcherPriority redrawPriority)
|
|
{
|
|
VerifyAccess();
|
|
ClearVisualLines();
|
|
InvalidateMeasure(redrawPriority);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Causes the text editor to regenerate the specified visual line.
|
|
/// </summary>
|
|
public void Redraw(VisualLine visualLine, DispatcherPriority redrawPriority = DispatcherPriority.Normal)
|
|
{
|
|
VerifyAccess();
|
|
if (allVisualLines.Remove(visualLine)) {
|
|
DisposeVisualLine(visualLine);
|
|
InvalidateMeasure(redrawPriority);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Causes the text editor to redraw all lines overlapping with the specified segment.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Causes a known layer to redraw.
|
|
/// This method does not invalidate visual lines;
|
|
/// use the <see cref="Redraw()"/> method to do that.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Causes a known layer to redraw.
|
|
/// This method does not invalidate visual lines;
|
|
/// use the <see cref="Redraw()"/> method to do that.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Causes the text editor to redraw all lines overlapping with the specified segment.
|
|
/// Does nothing if segment is null.
|
|
/// </summary>
|
|
public void Redraw(ISegment segment, DispatcherPriority redrawPriority = DispatcherPriority.Normal)
|
|
{
|
|
if (segment != null) {
|
|
Redraw(segment.Offset, segment.Length, redrawPriority);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invalidates all visual lines.
|
|
/// The caller of ClearVisualLines() must also call InvalidateMeasure() to ensure
|
|
/// that the visual lines will be recreated.
|
|
/// </summary>
|
|
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
|
|
/// <summary>
|
|
/// Gets the visual line that contains the document line with the specified number.
|
|
/// Returns null if the document line is outside the visible range.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<VisualLine> allVisualLines = new List<VisualLine>();
|
|
ReadOnlyCollection<VisualLine> visibleVisualLines;
|
|
double clippedPixelsOnTop;
|
|
List<VisualLine> newVisualLines;
|
|
|
|
/// <summary>
|
|
/// Gets the currently visible visual lines.
|
|
/// </summary>
|
|
/// <exception cref="VisualLinesInvalidException">
|
|
/// Gets thrown if there are invalid visual lines when this property is accessed.
|
|
/// You can use the <see cref="VisualLinesValid"/> property to check for this case,
|
|
/// or use the <see cref="EnsureVisualLines()"/> method to force creating the visual lines
|
|
/// when they are invalid.
|
|
/// </exception>
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations")]
|
|
public ReadOnlyCollection<VisualLine> VisualLines {
|
|
get {
|
|
if (visibleVisualLines == null)
|
|
throw new VisualLinesInvalidException();
|
|
return visibleVisualLines;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets whether the visual lines are valid.
|
|
/// Will return false after a call to Redraw().
|
|
/// Accessing the visual lines property will cause a <see cref="VisualLinesInvalidException"/>
|
|
/// if this property is <c>false</c>.
|
|
/// </summary>
|
|
public bool VisualLinesValid {
|
|
get { return visibleVisualLines != null; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public event EventHandler<VisualLineConstructionStartEventArgs> VisualLineConstructionStarting;
|
|
|
|
/// <summary>
|
|
/// Occurs when the TextView was measured and changed its visual lines.
|
|
/// </summary>
|
|
public event EventHandler VisualLinesChanged;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <exception cref="InvalidOperationException">The visual line build process is already running.
|
|
/// It is not allowed to call this method during the construction of a visual line.</exception>
|
|
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
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
const double AdditionalHorizontalScrollAmount = 3;
|
|
|
|
Size lastAvailableSize;
|
|
bool inMeasure;
|
|
|
|
/// <inheritdoc/>
|
|
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<VisualLine>();
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Build all VisualLines in the visible range.
|
|
/// </summary>
|
|
/// <returns>Width the longest line</returns>
|
|
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<VisualLine>();
|
|
|
|
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<VisualLine>(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);
|
|
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<TextLine>();
|
|
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
|
|
/// <summary>
|
|
/// Arrange implementation.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
InvalidateCursor();
|
|
|
|
return finalSize;
|
|
}
|
|
#endregion
|
|
|
|
#region Render
|
|
readonly ObserveAddRemoveCollection<IBackgroundRenderer> backgroundRenderers;
|
|
|
|
/// <summary>
|
|
/// Gets the list of background renderers.
|
|
/// </summary>
|
|
public IList<IBackgroundRenderer> BackgroundRenderers {
|
|
get { return backgroundRenderers; }
|
|
}
|
|
|
|
void BackgroundRenderer_Added(IBackgroundRenderer renderer)
|
|
{
|
|
ConnectToTextView(renderer);
|
|
InvalidateLayer(renderer.Layer);
|
|
}
|
|
|
|
void BackgroundRenderer_Removed(IBackgroundRenderer renderer)
|
|
{
|
|
DisconnectFromTextView(renderer);
|
|
InvalidateLayer(renderer.Layer);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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<VisualLineDrawingVisual> 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
|
|
/// <summary>
|
|
/// Size of the document, in pixels.
|
|
/// </summary>
|
|
Size scrollExtent;
|
|
|
|
/// <summary>
|
|
/// Offset of the scroll position.
|
|
/// </summary>
|
|
Vector scrollOffset;
|
|
|
|
/// <summary>
|
|
/// Size of the viewport.
|
|
/// </summary>
|
|
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; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the horizontal scroll offset.
|
|
/// </summary>
|
|
public double HorizontalOffset {
|
|
get { return scrollOffset.X; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the vertical scroll offset.
|
|
/// </summary>
|
|
public double VerticalOffset {
|
|
get { return scrollOffset.Y; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the scroll offset;
|
|
/// </summary>
|
|
public Vector ScrollOffset {
|
|
get { return scrollOffset; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Occurs when the scroll offset has changed.
|
|
/// </summary>
|
|
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.
|
|
|
|
/// <summary>
|
|
/// Gets the width of a 'wide space' (the space width used for calculating the tab size).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
public double WideSpaceWidth {
|
|
get {
|
|
CalculateDefaultTextMetrics();
|
|
return wideSpaceWidth;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public double DefaultLineHeight {
|
|
get {
|
|
CalculateDefaultTextMetrics();
|
|
return defaultLineHeight;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the default baseline position. This is the difference between <see cref="VisualYPosition.TextTop"/>
|
|
/// and <see cref="VisualYPosition.Baseline"/> for a line containing regular text.
|
|
/// Lines that include formatted text or custom UI elements may have a different baseline.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scrolls the text view so that the specified rectangle gets visible.
|
|
/// </summary>
|
|
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
|
|
/// <inheritdoc/>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Updates the mouse cursor by calling <see cref="Mouse.UpdateCursor"/>, but with input priority.
|
|
/// </summary>
|
|
public static void InvalidateCursor()
|
|
{
|
|
if (!invalidCursor) {
|
|
invalidCursor = true;
|
|
Dispatcher.CurrentDispatcher.BeginInvoke(
|
|
DispatcherPriority.Input,
|
|
new Action(
|
|
delegate {
|
|
invalidCursor = false;
|
|
Mouse.UpdateCursor();
|
|
}));
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override void OnQueryCursor(QueryCursorEventArgs e)
|
|
{
|
|
VisualLineElement element = GetVisualLineElementFromPosition(e.GetPosition(this) + scrollOffset);
|
|
if (element != null) {
|
|
element.OnQueryCursor(e);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the visual top position (relative to start of document) from a document line number.
|
|
/// </summary>
|
|
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
|
|
/// <summary>
|
|
/// Gets the visual position from a text view position.
|
|
/// </summary>
|
|
/// <param name="position">The text view position.</param>
|
|
/// <param name="yPositionMode">The mode how to retrieve the Y position.</param>
|
|
/// <returns>The position in WPF device-independent pixels relative
|
|
/// to the top left corner of the document.</returns>
|
|
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, yPositionMode);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="visualPosition">The position in WPF device-independent pixels relative
|
|
/// to the top left corner of the document.</param>
|
|
/// <returns>The logical position, or null if the position is outside the document.</returns>
|
|
public TextViewPosition? GetPosition(Point visualPosition)
|
|
{
|
|
VerifyAccess();
|
|
if (this.Document == null)
|
|
throw ThrowUtil.NoDocumentAssigned();
|
|
VisualLine line = GetVisualLineFromVisualTop(visualPosition.Y);
|
|
if (line == null)
|
|
return null;
|
|
int visualColumn = line.GetVisualColumn(visualPosition);
|
|
int documentOffset = line.GetRelativeOffset(visualColumn) + line.FirstDocumentLine.Offset;
|
|
return new TextViewPosition(document.GetLocation(documentOffset), visualColumn);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="visualPosition">The position in WPF device-independent pixels relative
|
|
/// to the top left corner of the document.</param>
|
|
/// <returns>The logical position, or null if the position is outside the document.</returns>
|
|
public TextViewPosition? GetPositionFloor(Point visualPosition)
|
|
{
|
|
VerifyAccess();
|
|
if (this.Document == null)
|
|
throw ThrowUtil.NoDocumentAssigned();
|
|
VisualLine line = GetVisualLineFromVisualTop(visualPosition.Y);
|
|
if (line == null)
|
|
return null;
|
|
int visualColumn = line.GetVisualColumnFloor(visualPosition);
|
|
int documentOffset = line.GetRelativeOffset(visualColumn) + line.FirstDocumentLine.Offset;
|
|
return new TextViewPosition(document.GetLocation(documentOffset), visualColumn);
|
|
}
|
|
#endregion
|
|
|
|
#region Service Provider
|
|
readonly ServiceContainer services = new ServiceContainer();
|
|
|
|
/// <summary>
|
|
/// Gets a service container used to associate services with the text view.
|
|
/// </summary>
|
|
public ServiceContainer Services {
|
|
get { return services; }
|
|
}
|
|
|
|
object IServiceProvider.GetService(Type serviceType)
|
|
{
|
|
return services.GetService(serviceType);
|
|
}
|
|
|
|
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
|
|
/// <summary>
|
|
/// The PreviewMouseHover event.
|
|
/// </summary>
|
|
public static readonly RoutedEvent PreviewMouseHoverEvent =
|
|
EventManager.RegisterRoutedEvent("PreviewMouseHover", RoutingStrategy.Tunnel,
|
|
typeof(MouseEventHandler), typeof(TextView));
|
|
/// <summary>
|
|
/// The MouseHover event.
|
|
/// </summary>
|
|
public static readonly RoutedEvent MouseHoverEvent =
|
|
EventManager.RegisterRoutedEvent("MouseHover", RoutingStrategy.Bubble,
|
|
typeof(MouseEventHandler), typeof(TextView));
|
|
|
|
/// <summary>
|
|
/// The PreviewMouseHoverStopped event.
|
|
/// </summary>
|
|
public static readonly RoutedEvent PreviewMouseHoverStoppedEvent =
|
|
EventManager.RegisterRoutedEvent("PreviewMouseHoverStopped", RoutingStrategy.Tunnel,
|
|
typeof(MouseEventHandler), typeof(TextView));
|
|
/// <summary>
|
|
/// The MouseHoverStopped event.
|
|
/// </summary>
|
|
public static readonly RoutedEvent MouseHoverStoppedEvent =
|
|
EventManager.RegisterRoutedEvent("MouseHoverStopped", RoutingStrategy.Bubble,
|
|
typeof(MouseEventHandler), typeof(TextView));
|
|
|
|
|
|
/// <summary>
|
|
/// Occurs when the mouse has hovered over a fixed location for some time.
|
|
/// </summary>
|
|
public event MouseEventHandler PreviewMouseHover {
|
|
add { AddHandler(PreviewMouseHoverEvent, value); }
|
|
remove { RemoveHandler(PreviewMouseHoverEvent, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Occurs when the mouse has hovered over a fixed location for some time.
|
|
/// </summary>
|
|
public event MouseEventHandler MouseHover {
|
|
add { AddHandler(MouseHoverEvent, value); }
|
|
remove { RemoveHandler(MouseHoverEvent, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Occurs when the mouse had previously hovered but now started moving again.
|
|
/// </summary>
|
|
public event MouseEventHandler PreviewMouseHoverStopped {
|
|
add { AddHandler(PreviewMouseHoverStoppedEvent, value); }
|
|
remove { RemoveHandler(PreviewMouseHoverStoppedEvent, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Occurs when the mouse had previously hovered but now started moving again.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Collapses lines for the purpose of scrolling. <see cref="DocumentLine"/>s marked as collapsed will be hidden
|
|
/// and not used to start the generation of a <see cref="VisualLine"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This method is meant for <see cref="VisualLineElementGenerator"/>s that cause <see cref="VisualLine"/>s to span
|
|
/// multiple <see cref="DocumentLine"/>s. Do not call it without providing a corresponding
|
|
/// <see cref="VisualLineElementGenerator"/>.
|
|
/// If you want to create collapsible text sections, see <see cref="Folding.FoldingManager"/>.
|
|
///
|
|
/// 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 <see cref="CollapsedLineSection.Uncollapse()"/> on the
|
|
/// <see cref="CollapsedLineSection"/> returned from this method.
|
|
/// </remarks>
|
|
public CollapsedLineSection CollapseLines(DocumentLine start, DocumentLine end)
|
|
{
|
|
VerifyAccess();
|
|
if (heightTree == null)
|
|
throw ThrowUtil.NoDocumentAssigned();
|
|
return heightTree.CollapseText(start, end);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the height of the document.
|
|
/// </summary>
|
|
public double DocumentHeight {
|
|
get {
|
|
// return 0 if there is no document = no heightTree
|
|
return heightTree != null ? heightTree.TotalHeight : 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the document line at the specified visual position.
|
|
/// </summary>
|
|
public DocumentLine GetDocumentLineByVisualTop(double visualTop)
|
|
{
|
|
VerifyAccess();
|
|
if (heightTree == null)
|
|
throw ThrowUtil.NoDocumentAssigned();
|
|
return heightTree.GetLineByVisualPosition(visualTop);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The pen used to draw the column ruler.
|
|
/// <seealso cref="TextEditorOptions.ShowColumnRuler"/>
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets/Sets the pen used to draw the column ruler.
|
|
/// <seealso cref="TextEditorOptions.ShowColumnRuler"/>
|
|
/// </summary>
|
|
public Pen ColumnRulerPen {
|
|
get { return (Pen)GetValue(ColumnRulerPenProperty); }
|
|
set { SetValue(ColumnRulerPenProperty, value); }
|
|
}
|
|
}
|
|
}
|