//
//
//
//
// $Revision$
//
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
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.Utils;
namespace ICSharpCode.AvalonEdit.Gui
{
///
/// A virtualizing panel producing+showing s for a .
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable",
Justification = "The user usually doesn't work with TextView but with TextEditor; nulling the Document property is sufficient to dispose everything.")]
public class TextView : FrameworkElement, IScrollInfo, IWeakEventListener
{
#region Constructor
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")]
static TextView()
{
ClipToBoundsProperty.OverrideMetadata(typeof(TextView), new FrameworkPropertyMetadata(true));
}
///
/// Creates a new TextView instance.
///
public TextView()
{
elementGenerators.CollectionChanged += delegate { Redraw(); };
lineTransformers.CollectionChanged += delegate { Redraw(); };
backgroundRenderer.CollectionChanged += delegate { InvalidateVisual(); };
adorners = new UIElementCollection(this, this);
}
#endregion
#region Properties
///
/// Document property.
///
public static readonly DependencyProperty DocumentProperty
= TextEditor.DocumentProperty.AddOwner(
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);
}
double LineHeight {
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;
TextDocumentWeakEventManager.Changing.RemoveListener(oldValue, this);
}
this.document = newValue;
ClearScrollData();
ClearVisualLines();
if (newValue != null) {
TextDocumentWeakEventManager.Changing.AddListener(newValue, this);
heightTree = new HeightTree(newValue, LineHeight + 3);
formatter = TextFormatter.Create();
}
InvalidateMeasure(DispatcherPriority.Normal);
if (DocumentChanged != null)
DocumentChanged(this, EventArgs.Empty);
}
bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
if (managerType == typeof(TextDocumentWeakEventManager.Changing)) {
// put redraw into background so that other input events can be handled before the redraw
DocumentChangeEventArgs change = (DocumentChangeEventArgs)e;
Redraw(change.Offset, change.RemovalLength, DispatcherPriority.Background);
return true;
}
return false;
}
#endregion
readonly ObservableCollection elementGenerators = new ObservableCollection();
///
/// Gets a collection where element generators can be registered.
///
public ObservableCollection ElementGenerators {
get { return elementGenerators; }
}
readonly ObservableCollection lineTransformers = new ObservableCollection();
///
/// Gets a collection where line transformers can be registered.
///
public ObservableCollection LineTransformers {
get { return lineTransformers; }
}
readonly UIElementCollection adorners;
///
/// Gets a collection where text view adorners can be added.
///
public UIElementCollection Adorners {
get { return adorners; }
}
///
/// Causes the text editor to regenerate all visual lines.
///
public void Redraw()
{
Redraw(DispatcherPriority.Render);
}
///
/// 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)
{
VerifyAccess();
if (allVisualLines.Remove(visualLine)) {
visibleVisualLines = null;
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)
{
VerifyAccess();
if (allVisualLines.Count != 0 || visibleVisualLines != null) {
bool removedLine = 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 (!(lineEnd < offset || lineStart > offset + length)) {
removedLine = true;
allVisualLines.RemoveAt(i--);
DisposeVisualLine(visualLine);
}
}
if (removedLine) {
visibleVisualLines = null;
InvalidateMeasure(redrawPriority);
}
}
}
///
/// 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)
{
if (segment != null) {
Redraw(segment.Offset, segment.Length, redrawPriority);
}
}
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();
}
)
);
}
}
}
///
/// Waits for the visual lines to be built.
///
private void EnsureVisualLines()
{
Dispatcher.VerifyAccess();
if (visibleVisualLines == null) {
// increase priority for real Redraw
InvalidateMeasure(DispatcherPriority.Normal);
// force immediate re-measure
UpdateLayout();
}
}
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!");
}
visualLine.IsDisposed = true;
foreach (TextLine textLine in visualLine.TextLines) {
textLine.Dispose();
}
RemoveInlineObjects(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 (documentLine.Document != this.Document)
throw new InvalidOperationException("Line belongs to wrong document");
VerifyAccess();
VisualLine l = GetVisualLine(documentLine.LineNumber);
if (l == null) {
TextRunProperties globalTextRunProperties = CreateGlobalTextRunProperties();
TextParagraphProperties paragraphProperties = CreateParagraphProperties(globalTextRunProperties);
while (heightTree.GetIsCollapsed(documentLine)) {
documentLine = heightTree.GetLineByNumber(documentLine.LineNumber - 1);
}
l = BuildVisualLine(documentLine,
globalTextRunProperties, paragraphProperties,
elementGenerators.ToArray(), lineTransformers.ToArray(),
lastAvailableSize);
l.VisualTop = heightTree.GetVisualPosition(documentLine);
allVisualLines.Add(l);
}
return l;
}
///
/// Collapses lines for the purpose of scrolling. 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 .
///
public CollapsedLineSection CollapseLines(DocumentLine start, DocumentLine end)
{
VerifyAccess();
return heightTree.CollapseText(start, end);
}
///
/// Gets the height of the document.
///
public double DocumentHeight {
get { return heightTree.TotalHeight; }
}
#region Measure
TextFormatter formatter;
List allVisualLines = new List();
ReadOnlyCollection visibleVisualLines;
double clippedPixelsOnTop;
///
/// Gets the currently visible visual lines.
///
public ReadOnlyCollection VisualLines {
get {
EnsureVisualLines();
return visibleVisualLines;
}
}
///
/// Gets whether the visual lines are valid.
/// Will return false after a call to Redraw(). Accessing the visual lines property
/// will force immediate regeneration of valid lines.
///
public bool VisualLinesValid {
get { return visibleVisualLines != null; }
}
///
/// Occurs when the TextView was measured and changed its visual lines.
///
public event EventHandler VisualLinesChanged;
TextRunProperties CreateGlobalTextRunProperties()
{
return new GlobalTextRunProperties {
typeface = this.CreateTypeface(),
fontRenderingEmSize = LineHeight,
foregroundBrush = (Brush)GetValue(Control.ForegroundProperty),
cultureInfo = CultureInfo.CurrentCulture
};
}
TextParagraphProperties CreateParagraphProperties(TextRunProperties defaultTextRunProperties)
{
return new VisualLineTextParagraphProperties {
defaultTextRunProperties = defaultTextRunProperties,
textWrapping = canHorizontallyScroll ? TextWrapping.NoWrap : TextWrapping.Wrap,
tabSize = 4 * WideSpaceWidth
};
}
Size lastAvailableSize;
List newVisualLines;
///
/// Measure implementation.
///
protected override Size MeasureOverride(Size availableSize)
{
if (!canHorizontallyScroll && !availableSize.Width.IsClose(lastAvailableSize.Width))
ClearVisualLines();
lastAvailableSize = availableSize;
return DoMeasure(availableSize);
}
///
/// Immediately performs the text creation.
///
/// The size of the text view.
///
Size DoMeasure(Size availableSize)
{
bool isRealMeasure = true;
if (isRealMeasure)
RemoveInlineObjectsNow();
if (document == null)
return Size.Empty;
TextRunProperties globalTextRunProperties = CreateGlobalTextRunProperties();
TextParagraphProperties paragraphProperties = CreateParagraphProperties(globalTextRunProperties);
InvalidateVisual(); // = InvalidateArrange+InvalidateRender
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);
Debug.Assert(clippedPixelsOnTop >= 0);
newVisualLines = new List();
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;
int visualLineEndLineNumber = visualLine.LastDocumentLine.LineNumber;
if (visualLineEndLineNumber == document.LineCount)
nextLine = null;
else
nextLine = document.GetLineByNumber(visualLineEndLineNumber + 1);
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);
}
if (isRealMeasure)
RemoveInlineObjectsNow();
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.");
}
SetScrollData(availableSize,
new Size(maxWidth, heightTree.TotalHeight),
scrollOffset);
if (VisualLinesChanged != null)
VisualLinesChanged(this, EventArgs.Empty);
if (canHorizontallyScroll) {
return availableSize;
} else {
return new Size(maxWidth, availableSize.Height);
}
}
VisualLine BuildVisualLine(DocumentLine documentLine,
TextRunProperties globalTextRunProperties,
TextParagraphProperties paragraphProperties,
VisualLineElementGenerator[] elementGeneratorsArray,
IVisualLineTransformer[] lineTransformersArray,
Size availableSize)
{
if (heightTree.GetIsCollapsed(documentLine))
throw new InvalidOperationException("Trying to build visual line from collapsed line");
Debug.WriteLine("Building line " + documentLine.LineNumber);
VisualLine visualLine = new VisualLine(documentLine);
VisualLineTextSource textSource = new VisualLineTextSource(visualLine) {
Document = document,
GlobalTextRunProperties = globalTextRunProperties,
TextView = this
};
visualLine.ConstructVisualElements(textSource, elementGeneratorsArray);
#if DEBUG
for (int i = visualLine.FirstDocumentLine.LineNumber + 1; i <= visualLine.LastDocumentLine.LineNumber; i++) {
if (!heightTree.GetIsCollapsed(document.GetLineByNumber(i)))
throw new InvalidOperationException("Line " + i + " was skipped by a VisualLineElementGenerator, but it is not collapsed.");
}
#endif
visualLine.RunTransformers(textSource, lineTransformersArray);
// now construct textLines:
int textOffset = 0;
TextLineBreak lastLineBreak = null;
var textLines = new List();
while (textOffset <= visualLine.VisualLength) {
TextLine textLine = formatter.FormatLine(
textSource,
textOffset,
availableSize.Width,
paragraphProperties,
lastLineBreak
);
textLines.Add(textLine);
textOffset += textLine.Length;
lastLineBreak = textLine.GetTextLineBreak();
}
visualLine.SetTextLines(textLines);
heightTree.SetHeight(visualLine.FirstDocumentLine, visualLine.Height);
return visualLine;
}
#endregion
#region Inline object handling
List inlineObjects = new List();
///
/// Adds a new inline object.
///
internal void AddInlineObject(InlineObjectRun inlineObject)
{
inlineObjects.Add(inlineObject);
AddVisualChild(inlineObject.Element);
inlineObject.Element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
}
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.
visualLinesWithOutstandingInlineObjects.Add(visualLine);
}
void RemoveInlineObjectsNow()
{
inlineObjects.RemoveAll(
ior => {
if (visualLinesWithOutstandingInlineObjects.Contains(ior.VisualLine)) {
ior.VisualLine = null;
RemoveVisualChild(ior.Element);
return true;
}
return false;
});
visualLinesWithOutstandingInlineObjects.Clear();
}
///
/// Removes the inline object that displays the specified UIElement.
///
public void RemoveInlineObject(UIElement element)
{
inlineObjects.RemoveAll(
ior => {
if (ior.Element == element) {
ior.VisualLine = null;
RemoveVisualChild(ior.Element);
return true;
}
return false;
});
}
///
protected override int VisualChildrenCount {
get { return inlineObjects.Count + adorners.Count; }
}
///
protected override Visual GetVisualChild(int index)
{
if (index < inlineObjects.Count)
return inlineObjects[index].Element;
else
return adorners[index - inlineObjects.Count];
}
#endregion
#region Arrange
///
/// Arrange implementation.
///
protected override Size ArrangeOverride(Size 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;
foreach (UIElement adorner in adorners) {
adorner.Arrange(new Rect(new Point(0, 0), finalSize));
}
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 ObservableCollection backgroundRenderer = new ObservableCollection();
///
/// Gets a collection where line transformers can be registered.
///
public ObservableCollection BackgroundRenderer {
get { return backgroundRenderer; }
}
///
protected override void OnRender(DrawingContext drawingContext)
{
foreach (IBackgroundRenderer r in backgroundRenderer)
r.Draw(drawingContext);
Point pos = new Point(-scrollOffset.X, -clippedPixelsOnTop);
foreach (VisualLine visualLine in allVisualLines) {
foreach (TextLine textLine in visualLine.TextLines) {
textLine.Draw(drawingContext, pos, InvertAxes.None);
pos.Y += textLine.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 (!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 - LineHeight);
}
void IScrollInfo.LineDown()
{
((IScrollInfo)this).SetVerticalOffset(scrollOffset.Y + LineHeight);
}
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 * LineHeight));
OnScrollChange();
}
void IScrollInfo.MouseWheelDown()
{
((IScrollInfo)this).SetVerticalOffset(
scrollOffset.Y + (SystemParameters.WheelScrollLines * LineHeight));
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();
}
double WideSpaceWidth {
get {
return LineHeight / 2;
}
}
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();
}
}
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(rectangle);
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
///
/// Gets the document line at the specified visual position.
///
public DocumentLine GetDocumentLineByVisualTop(double visualTop)
{
VerifyAccess();
if (heightTree == null)
throw new InvalidOperationException();
return heightTree.GetLineByVisualPosition(visualTop);
}
#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 input priority.
///
public static void InvalidateCursor()
{
if (!invalidCursor) {
invalidCursor = true;
Dispatcher.CurrentDispatcher.BeginInvoke(
DispatcherPriority.Input,
new Action(
delegate {
invalidCursor = false;
Mouse.UpdateCursor();
}));
}
}
///
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);
}
}
}
///
/// 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).
/// You may want to call () before calling this method.
///
public VisualLine GetVisualLineFromVisualTop(double visualTop)
{
foreach (VisualLine vl in this.VisualLines) {
if (visualTop < vl.VisualTop)
continue;
if (visualTop < vl.VisualTop + vl.Height)
return vl;
}
return null;
}
VisualLineElement GetVisualLineElementFromPosition(Point visualPosition)
{
VisualLine vl = GetVisualLineFromVisualTop(visualPosition.Y);
if (vl != null) {
int column = vl.GetVisualColumn(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
}
}