682 lines
23 KiB
C#
682 lines
23 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.Linq;
|
|||
|
|
using System.Windows.Controls;
|
|||
|
|
using System.Windows.Media;
|
|||
|
|
using ICSharpCode.AvalonEdit.Utils;
|
|||
|
|
using System;
|
|||
|
|
using System.Collections.Generic;
|
|||
|
|
using System.Collections.ObjectModel;
|
|||
|
|
using System.Diagnostics;
|
|||
|
|
using System.Windows;
|
|||
|
|
using System.Windows.Documents;
|
|||
|
|
using System.Windows.Media.TextFormatting;
|
|||
|
|
using ICSharpCode.AvalonEdit.Document;
|
|||
|
|
|
|||
|
|
namespace ICSharpCode.AvalonEdit.Rendering
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// Represents a visual line in the document.
|
|||
|
|
/// A visual line usually corresponds to one DocumentLine, but it can span multiple lines if
|
|||
|
|
/// all but the first are collapsed.
|
|||
|
|
/// </summary>
|
|||
|
|
public sealed class VisualLine
|
|||
|
|
{
|
|||
|
|
enum LifetimePhase : byte
|
|||
|
|
{
|
|||
|
|
Generating,
|
|||
|
|
Transforming,
|
|||
|
|
Live,
|
|||
|
|
Disposed
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
TextView textView;
|
|||
|
|
List<VisualLineElement> elements;
|
|||
|
|
internal bool hasInlineObjects;
|
|||
|
|
LifetimePhase phase;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets the document to which this VisualLine belongs.
|
|||
|
|
/// </summary>
|
|||
|
|
public TextDocument Document { get; private set; }
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets the first document line displayed by this visual line.
|
|||
|
|
/// </summary>
|
|||
|
|
public DocumentLine FirstDocumentLine { get; private set; }
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets the last document line displayed by this visual line.
|
|||
|
|
/// </summary>
|
|||
|
|
public DocumentLine LastDocumentLine { get; private set; }
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets a read-only collection of line elements.
|
|||
|
|
/// </summary>
|
|||
|
|
public ReadOnlyCollection<VisualLineElement> Elements { get; private set; }
|
|||
|
|
|
|||
|
|
ReadOnlyCollection<TextLine> textLines;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets a read-only collection of text lines.
|
|||
|
|
/// </summary>
|
|||
|
|
public ReadOnlyCollection<TextLine> TextLines {
|
|||
|
|
get {
|
|||
|
|
if (phase < LifetimePhase.Live)
|
|||
|
|
throw new InvalidOperationException();
|
|||
|
|
return textLines;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets the start offset of the VisualLine inside the document.
|
|||
|
|
/// This is equivalent to <c>FirstDocumentLine.Offset</c>.
|
|||
|
|
/// </summary>
|
|||
|
|
public int StartOffset {
|
|||
|
|
get {
|
|||
|
|
return FirstDocumentLine.Offset;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Length in visual line coordinates.
|
|||
|
|
/// </summary>
|
|||
|
|
public int VisualLength { get; private set; }
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Length in visual line coordinates including the end of line marker, if TextEditorOptions.ShowEndOfLine is enabled.
|
|||
|
|
/// </summary>
|
|||
|
|
public int VisualLengthWithEndOfLineMarker {
|
|||
|
|
get {
|
|||
|
|
int length = VisualLength;
|
|||
|
|
if (textView.Options.ShowEndOfLine && LastDocumentLine.NextLine != null) length++;
|
|||
|
|
return length;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets the height of the visual line in device-independent pixels.
|
|||
|
|
/// </summary>
|
|||
|
|
public double Height { get; private set; }
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets the Y position of the line. This is measured in device-independent pixels relative to the start of the document.
|
|||
|
|
/// </summary>
|
|||
|
|
public double VisualTop { get; internal set; }
|
|||
|
|
|
|||
|
|
internal VisualLine(TextView textView, DocumentLine firstDocumentLine)
|
|||
|
|
{
|
|||
|
|
Debug.Assert(textView != null);
|
|||
|
|
Debug.Assert(firstDocumentLine != null);
|
|||
|
|
this.textView = textView;
|
|||
|
|
this.Document = textView.Document;
|
|||
|
|
this.FirstDocumentLine = firstDocumentLine;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
internal void ConstructVisualElements(ITextRunConstructionContext context, VisualLineElementGenerator[] generators)
|
|||
|
|
{
|
|||
|
|
Debug.Assert(phase == LifetimePhase.Generating);
|
|||
|
|
foreach (VisualLineElementGenerator g in generators) {
|
|||
|
|
g.StartGeneration(context);
|
|||
|
|
}
|
|||
|
|
elements = new List<VisualLineElement>();
|
|||
|
|
PerformVisualElementConstruction(generators);
|
|||
|
|
foreach (VisualLineElementGenerator g in generators) {
|
|||
|
|
g.FinishGeneration();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var globalTextRunProperties = context.GlobalTextRunProperties;
|
|||
|
|
foreach (var element in elements) {
|
|||
|
|
element.SetTextRunProperties(new VisualLineElementTextRunProperties(globalTextRunProperties));
|
|||
|
|
}
|
|||
|
|
this.Elements = elements.AsReadOnly();
|
|||
|
|
CalculateOffsets();
|
|||
|
|
phase = LifetimePhase.Transforming;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void PerformVisualElementConstruction(VisualLineElementGenerator[] generators)
|
|||
|
|
{
|
|||
|
|
TextDocument document = this.Document;
|
|||
|
|
int offset = FirstDocumentLine.Offset;
|
|||
|
|
int currentLineEnd = offset + FirstDocumentLine.Length;
|
|||
|
|
LastDocumentLine = FirstDocumentLine;
|
|||
|
|
int askInterestOffset = 0; // 0 or 1
|
|||
|
|
while (offset + askInterestOffset <= currentLineEnd) {
|
|||
|
|
int textPieceEndOffset = currentLineEnd;
|
|||
|
|
foreach (VisualLineElementGenerator g in generators) {
|
|||
|
|
g.cachedInterest = g.GetFirstInterestedOffset(offset + askInterestOffset);
|
|||
|
|
if (g.cachedInterest != -1) {
|
|||
|
|
if (g.cachedInterest < offset)
|
|||
|
|
throw new ArgumentOutOfRangeException(g.GetType().Name + ".GetFirstInterestedOffset",
|
|||
|
|
g.cachedInterest,
|
|||
|
|
"GetFirstInterestedOffset must not return an offset less than startOffset. Return -1 to signal no interest.");
|
|||
|
|
if (g.cachedInterest < textPieceEndOffset)
|
|||
|
|
textPieceEndOffset = g.cachedInterest;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
Debug.Assert(textPieceEndOffset >= offset);
|
|||
|
|
if (textPieceEndOffset > offset) {
|
|||
|
|
int textPieceLength = textPieceEndOffset - offset;
|
|||
|
|
elements.Add(new VisualLineText(this, textPieceLength));
|
|||
|
|
offset = textPieceEndOffset;
|
|||
|
|
}
|
|||
|
|
// If no elements constructed / only zero-length elements constructed:
|
|||
|
|
// do not asking the generators again for the same location (would cause endless loop)
|
|||
|
|
askInterestOffset = 1;
|
|||
|
|
foreach (VisualLineElementGenerator g in generators) {
|
|||
|
|
if (g.cachedInterest == offset) {
|
|||
|
|
VisualLineElement element = g.ConstructElement(offset);
|
|||
|
|
if (element != null) {
|
|||
|
|
elements.Add(element);
|
|||
|
|
if (element.DocumentLength > 0) {
|
|||
|
|
// a non-zero-length element was constructed
|
|||
|
|
askInterestOffset = 0;
|
|||
|
|
offset += element.DocumentLength;
|
|||
|
|
if (offset > currentLineEnd) {
|
|||
|
|
DocumentLine newEndLine = document.GetLineByOffset(offset);
|
|||
|
|
currentLineEnd = newEndLine.Offset + newEndLine.Length;
|
|||
|
|
this.LastDocumentLine = newEndLine;
|
|||
|
|
if (currentLineEnd < offset) {
|
|||
|
|
throw new InvalidOperationException(
|
|||
|
|
"The VisualLineElementGenerator " + g.GetType().Name +
|
|||
|
|
" produced an element which ends within the line delimiter");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void CalculateOffsets()
|
|||
|
|
{
|
|||
|
|
int visualOffset = 0;
|
|||
|
|
int textOffset = 0;
|
|||
|
|
foreach (VisualLineElement element in elements) {
|
|||
|
|
element.VisualColumn = visualOffset;
|
|||
|
|
element.RelativeTextOffset = textOffset;
|
|||
|
|
visualOffset += element.VisualLength;
|
|||
|
|
textOffset += element.DocumentLength;
|
|||
|
|
}
|
|||
|
|
VisualLength = visualOffset;
|
|||
|
|
Debug.Assert(textOffset == LastDocumentLine.EndOffset - FirstDocumentLine.Offset);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
internal void RunTransformers(ITextRunConstructionContext context, IVisualLineTransformer[] transformers)
|
|||
|
|
{
|
|||
|
|
Debug.Assert(phase == LifetimePhase.Transforming);
|
|||
|
|
foreach (IVisualLineTransformer transformer in transformers) {
|
|||
|
|
transformer.Transform(context, elements);
|
|||
|
|
}
|
|||
|
|
// For some strange reason, WPF requires that either all or none of the typography properties are set.
|
|||
|
|
if (elements.Any(e => e.TextRunProperties.TypographyProperties != null)) {
|
|||
|
|
// Fix typographic properties
|
|||
|
|
foreach (VisualLineElement element in elements) {
|
|||
|
|
if (element.TextRunProperties.TypographyProperties == null) {
|
|||
|
|
element.TextRunProperties.SetTypographyProperties(new DefaultTextRunTypographyProperties());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
phase = LifetimePhase.Live;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Replaces the single element at <paramref name="elementIndex"/> with the specified elements.
|
|||
|
|
/// The replacement operation must preserve the document length, but may change the visual length.
|
|||
|
|
/// </summary>
|
|||
|
|
/// <remarks>
|
|||
|
|
/// This method may only be called by line transformers.
|
|||
|
|
/// </remarks>
|
|||
|
|
public void ReplaceElement(int elementIndex, params VisualLineElement[] newElements)
|
|||
|
|
{
|
|||
|
|
ReplaceElement(elementIndex, 1, newElements);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Replaces <paramref name="count"/> elements starting at <paramref name="elementIndex"/> with the specified elements.
|
|||
|
|
/// The replacement operation must preserve the document length, but may change the visual length.
|
|||
|
|
/// </summary>
|
|||
|
|
/// <remarks>
|
|||
|
|
/// This method may only be called by line transformers.
|
|||
|
|
/// </remarks>
|
|||
|
|
public void ReplaceElement(int elementIndex, int count, params VisualLineElement[] newElements)
|
|||
|
|
{
|
|||
|
|
if (phase != LifetimePhase.Transforming)
|
|||
|
|
throw new InvalidOperationException("This method may only be called by line transformers.");
|
|||
|
|
int oldDocumentLength = 0;
|
|||
|
|
for (int i = elementIndex; i < elementIndex + count; i++) {
|
|||
|
|
oldDocumentLength += elements[i].DocumentLength;
|
|||
|
|
}
|
|||
|
|
int newDocumentLength = 0;
|
|||
|
|
foreach (var newElement in newElements) {
|
|||
|
|
newDocumentLength += newElement.DocumentLength;
|
|||
|
|
}
|
|||
|
|
if (oldDocumentLength != newDocumentLength)
|
|||
|
|
throw new InvalidOperationException("Old elements have document length " + oldDocumentLength + ", but new elements have length " + newDocumentLength);
|
|||
|
|
elements.RemoveRange(elementIndex, count);
|
|||
|
|
elements.InsertRange(elementIndex, newElements);
|
|||
|
|
CalculateOffsets();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
internal void SetTextLines(List<TextLine> textLines)
|
|||
|
|
{
|
|||
|
|
this.textLines = textLines.AsReadOnly();
|
|||
|
|
Height = 0;
|
|||
|
|
foreach (TextLine line in textLines)
|
|||
|
|
Height += line.Height;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets the visual column from a document offset relative to the first line start.
|
|||
|
|
/// </summary>
|
|||
|
|
public int GetVisualColumn(int relativeTextOffset)
|
|||
|
|
{
|
|||
|
|
ThrowUtil.CheckNotNegative(relativeTextOffset, "relativeTextOffset");
|
|||
|
|
foreach (VisualLineElement element in elements) {
|
|||
|
|
if (element.RelativeTextOffset <= relativeTextOffset
|
|||
|
|
&& element.RelativeTextOffset + element.DocumentLength >= relativeTextOffset)
|
|||
|
|
{
|
|||
|
|
return element.GetVisualColumn(relativeTextOffset);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return VisualLength;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets the document offset (relative to the first line start) from a visual column.
|
|||
|
|
/// </summary>
|
|||
|
|
public int GetRelativeOffset(int visualColumn)
|
|||
|
|
{
|
|||
|
|
ThrowUtil.CheckNotNegative(visualColumn, "visualColumn");
|
|||
|
|
int documentLength = 0;
|
|||
|
|
foreach (VisualLineElement element in elements) {
|
|||
|
|
if (element.VisualColumn <= visualColumn
|
|||
|
|
&& element.VisualColumn + element.VisualLength > visualColumn)
|
|||
|
|
{
|
|||
|
|
return element.GetRelativeOffset(visualColumn);
|
|||
|
|
}
|
|||
|
|
documentLength += element.DocumentLength;
|
|||
|
|
}
|
|||
|
|
return documentLength;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets the text line containing the specified visual column.
|
|||
|
|
/// </summary>
|
|||
|
|
public TextLine GetTextLine(int visualColumn)
|
|||
|
|
{
|
|||
|
|
if (visualColumn < 0)
|
|||
|
|
throw new ArgumentOutOfRangeException("visualColumn");
|
|||
|
|
if (visualColumn >= VisualLengthWithEndOfLineMarker)
|
|||
|
|
return TextLines[TextLines.Count - 1];
|
|||
|
|
foreach (TextLine line in TextLines) {
|
|||
|
|
if (visualColumn < line.Length)
|
|||
|
|
return line;
|
|||
|
|
else
|
|||
|
|
visualColumn -= line.Length;
|
|||
|
|
}
|
|||
|
|
throw new InvalidOperationException("Shouldn't happen (VisualLength incorrect?)");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets the visual top from the specified text line.
|
|||
|
|
/// </summary>
|
|||
|
|
/// <returns>Distance in device-independent pixels
|
|||
|
|
/// from the top of the document to the top of the specified text line.</returns>
|
|||
|
|
public double GetTextLineVisualYPosition(TextLine textLine, VisualYPosition yPositionMode)
|
|||
|
|
{
|
|||
|
|
if (textLine == null)
|
|||
|
|
throw new ArgumentNullException("textLine");
|
|||
|
|
double pos = VisualTop;
|
|||
|
|
foreach (TextLine tl in TextLines) {
|
|||
|
|
if (tl == textLine) {
|
|||
|
|
switch (yPositionMode) {
|
|||
|
|
case VisualYPosition.LineTop:
|
|||
|
|
return pos;
|
|||
|
|
case VisualYPosition.LineMiddle:
|
|||
|
|
return pos + tl.Height / 2;
|
|||
|
|
case VisualYPosition.LineBottom:
|
|||
|
|
return pos + tl.Height;
|
|||
|
|
case VisualYPosition.TextTop:
|
|||
|
|
return pos + tl.Baseline - textView.DefaultBaseline;
|
|||
|
|
case VisualYPosition.TextBottom:
|
|||
|
|
return pos + tl.Baseline - textView.DefaultBaseline + textView.DefaultLineHeight;
|
|||
|
|
case VisualYPosition.TextMiddle:
|
|||
|
|
return pos + tl.Baseline - textView.DefaultBaseline + textView.DefaultLineHeight / 2;
|
|||
|
|
case VisualYPosition.Baseline:
|
|||
|
|
return pos + tl.Baseline;
|
|||
|
|
default:
|
|||
|
|
throw new ArgumentException("Invalid yPositionMode:" + yPositionMode);
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
pos += tl.Height;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
throw new ArgumentException("textLine is not a line in this VisualLine");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets the start visual column from the specified text line.
|
|||
|
|
/// </summary>
|
|||
|
|
public int GetTextLineVisualStartColumn(TextLine textLine)
|
|||
|
|
{
|
|||
|
|
if (!TextLines.Contains(textLine))
|
|||
|
|
throw new ArgumentException("textLine is not a line in this VisualLine");
|
|||
|
|
int col = 0;
|
|||
|
|
foreach (TextLine tl in TextLines) {
|
|||
|
|
if (tl == textLine)
|
|||
|
|
break;
|
|||
|
|
else
|
|||
|
|
col += tl.Length;
|
|||
|
|
}
|
|||
|
|
return col;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets a TextLine by the visual position.
|
|||
|
|
/// </summary>
|
|||
|
|
public TextLine GetTextLineByVisualYPosition(double visualTop)
|
|||
|
|
{
|
|||
|
|
const double epsilon = 0.0001;
|
|||
|
|
double pos = this.VisualTop;
|
|||
|
|
foreach (TextLine tl in TextLines) {
|
|||
|
|
pos += tl.Height;
|
|||
|
|
if (visualTop + epsilon < pos)
|
|||
|
|
return tl;
|
|||
|
|
}
|
|||
|
|
return TextLines[TextLines.Count - 1];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets the visual position from the specified visualColumn.
|
|||
|
|
/// </summary>
|
|||
|
|
/// <returns>Position in device-independent pixels
|
|||
|
|
/// relative to the top left of the document.</returns>
|
|||
|
|
public Point GetVisualPosition(int visualColumn, VisualYPosition yPositionMode)
|
|||
|
|
{
|
|||
|
|
TextLine textLine = GetTextLine(visualColumn);
|
|||
|
|
double xPos = GetTextLineVisualXPosition(textLine, visualColumn);
|
|||
|
|
double yPos = GetTextLineVisualYPosition(textLine, yPositionMode);
|
|||
|
|
return new Point(xPos, yPos);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets the distance to the left border of the text area of the specified visual column.
|
|||
|
|
/// The visual column must belong to the specified text line.
|
|||
|
|
/// </summary>
|
|||
|
|
public double GetTextLineVisualXPosition(TextLine textLine, int visualColumn)
|
|||
|
|
{
|
|||
|
|
if (textLine == null)
|
|||
|
|
throw new ArgumentNullException("textLine");
|
|||
|
|
double xPos = textLine.GetDistanceFromCharacterHit(
|
|||
|
|
new CharacterHit(Math.Min(visualColumn, VisualLengthWithEndOfLineMarker), 0));
|
|||
|
|
if (visualColumn > VisualLengthWithEndOfLineMarker) {
|
|||
|
|
xPos += (visualColumn - VisualLengthWithEndOfLineMarker) * textView.WideSpaceWidth;
|
|||
|
|
}
|
|||
|
|
return xPos;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets the visual column from a document position (relative to top left of the document).
|
|||
|
|
/// If the user clicks between two visual columns, rounds to the nearest column.
|
|||
|
|
/// </summary>
|
|||
|
|
public int GetVisualColumn(Point point)
|
|||
|
|
{
|
|||
|
|
return GetVisualColumn(point, textView.Options.EnableVirtualSpace);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets the visual column from a document position (relative to top left of the document).
|
|||
|
|
/// If the user clicks between two visual columns, rounds to the nearest column.
|
|||
|
|
/// </summary>
|
|||
|
|
public int GetVisualColumn(Point point, bool allowVirtualSpace)
|
|||
|
|
{
|
|||
|
|
return GetVisualColumn(GetTextLineByVisualYPosition(point.Y), point.X, allowVirtualSpace);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets the visual column from a document position (relative to top left of the document).
|
|||
|
|
/// If the user clicks between two visual columns, rounds to the nearest column.
|
|||
|
|
/// </summary>
|
|||
|
|
public int GetVisualColumn(TextLine textLine, double xPos, bool allowVirtualSpace)
|
|||
|
|
{
|
|||
|
|
if (xPos > textLine.WidthIncludingTrailingWhitespace) {
|
|||
|
|
if (allowVirtualSpace && textLine == TextLines[TextLines.Count - 1]) {
|
|||
|
|
int virtualX = (int)Math.Round((xPos - textLine.WidthIncludingTrailingWhitespace) / textView.WideSpaceWidth);
|
|||
|
|
return VisualLengthWithEndOfLineMarker + virtualX;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
CharacterHit ch = textLine.GetCharacterHitFromDistance(xPos);
|
|||
|
|
return ch.FirstCharacterIndex + ch.TrailingLength;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Validates the visual column and returns the correct one.
|
|||
|
|
/// </summary>
|
|||
|
|
public int ValidateVisualColumn(TextViewPosition position, bool allowVirtualSpace)
|
|||
|
|
{
|
|||
|
|
return ValidateVisualColumn(Document.GetOffset(position.Location), position.VisualColumn, allowVirtualSpace);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Validates the visual column and returns the correct one.
|
|||
|
|
/// </summary>
|
|||
|
|
public int ValidateVisualColumn(int offset, int visualColumn, bool allowVirtualSpace)
|
|||
|
|
{
|
|||
|
|
int firstDocumentLineOffset = this.FirstDocumentLine.Offset;
|
|||
|
|
if (visualColumn < 0) {
|
|||
|
|
return GetVisualColumn(offset - firstDocumentLineOffset);
|
|||
|
|
} else {
|
|||
|
|
int offsetFromVisualColumn = GetRelativeOffset(visualColumn);
|
|||
|
|
offsetFromVisualColumn += firstDocumentLineOffset;
|
|||
|
|
if (offsetFromVisualColumn != offset) {
|
|||
|
|
return GetVisualColumn(offset - firstDocumentLineOffset);
|
|||
|
|
} else {
|
|||
|
|
if (visualColumn > VisualLength && !allowVirtualSpace) {
|
|||
|
|
return VisualLength;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return visualColumn;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets the visual column from a document position (relative to top left of the document).
|
|||
|
|
/// If the user clicks between two visual columns, returns the first of those columns.
|
|||
|
|
/// </summary>
|
|||
|
|
public int GetVisualColumnFloor(Point point)
|
|||
|
|
{
|
|||
|
|
return GetVisualColumnFloor(point, textView.Options.EnableVirtualSpace);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets the visual column from a document position (relative to top left of the document).
|
|||
|
|
/// If the user clicks between two visual columns, returns the first of those columns.
|
|||
|
|
/// </summary>
|
|||
|
|
public int GetVisualColumnFloor(Point point, bool allowVirtualSpace)
|
|||
|
|
{
|
|||
|
|
TextLine textLine = GetTextLineByVisualYPosition(point.Y);
|
|||
|
|
if (point.X > textLine.WidthIncludingTrailingWhitespace) {
|
|||
|
|
if (allowVirtualSpace && textLine == TextLines[TextLines.Count - 1]) {
|
|||
|
|
// clicking virtual space in the last line
|
|||
|
|
int virtualX = (int)((point.X - textLine.WidthIncludingTrailingWhitespace) / textView.WideSpaceWidth);
|
|||
|
|
return VisualLengthWithEndOfLineMarker + virtualX;
|
|||
|
|
} else {
|
|||
|
|
// GetCharacterHitFromDistance returns a hit with FirstCharacterIndex=last character in line
|
|||
|
|
// and TrailingLength=1 when clicking behind the line, so the floor function needs to handle this case
|
|||
|
|
// specially and return the line's end column instead.
|
|||
|
|
return GetTextLineVisualStartColumn(textLine) + textLine.Length;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
CharacterHit ch = textLine.GetCharacterHitFromDistance(point.X);
|
|||
|
|
return ch.FirstCharacterIndex;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets whether the visual line was disposed.
|
|||
|
|
/// </summary>
|
|||
|
|
public bool IsDisposed {
|
|||
|
|
get { return phase == LifetimePhase.Disposed; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
internal void Dispose()
|
|||
|
|
{
|
|||
|
|
if (phase == LifetimePhase.Disposed)
|
|||
|
|
return;
|
|||
|
|
Debug.Assert(phase == LifetimePhase.Live);
|
|||
|
|
phase = LifetimePhase.Disposed;
|
|||
|
|
foreach (TextLine textLine in TextLines) {
|
|||
|
|
textLine.Dispose();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets the next possible caret position after visualColumn, or -1 if there is no caret position.
|
|||
|
|
/// </summary>
|
|||
|
|
public int GetNextCaretPosition(int visualColumn, LogicalDirection direction, CaretPositioningMode mode, bool allowVirtualSpace)
|
|||
|
|
{
|
|||
|
|
if (!HasStopsInVirtualSpace(mode))
|
|||
|
|
allowVirtualSpace = false;
|
|||
|
|
|
|||
|
|
if (elements.Count == 0) {
|
|||
|
|
// special handling for empty visual lines:
|
|||
|
|
if (allowVirtualSpace) {
|
|||
|
|
if (direction == LogicalDirection.Forward)
|
|||
|
|
return Math.Max(0, visualColumn + 1);
|
|||
|
|
else if (visualColumn > 0)
|
|||
|
|
return visualColumn - 1;
|
|||
|
|
else
|
|||
|
|
return -1;
|
|||
|
|
} else {
|
|||
|
|
// even though we don't have any elements,
|
|||
|
|
// there's a single caret stop at visualColumn 0
|
|||
|
|
if (visualColumn < 0 && direction == LogicalDirection.Forward)
|
|||
|
|
return 0;
|
|||
|
|
else if (visualColumn > 0 && direction == LogicalDirection.Backward)
|
|||
|
|
return 0;
|
|||
|
|
else
|
|||
|
|
return -1;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
int i;
|
|||
|
|
if (direction == LogicalDirection.Backward) {
|
|||
|
|
// Search Backwards:
|
|||
|
|
// If the last element doesn't handle line borders, return the line end as caret stop
|
|||
|
|
|
|||
|
|
if (visualColumn > this.VisualLength && !elements[elements.Count-1].HandlesLineBorders && HasImplicitStopAtLineEnd(mode)) {
|
|||
|
|
if (allowVirtualSpace)
|
|||
|
|
return visualColumn - 1;
|
|||
|
|
else
|
|||
|
|
return this.VisualLength;
|
|||
|
|
}
|
|||
|
|
// skip elements that start after or at visualColumn
|
|||
|
|
for (i = elements.Count - 1; i >= 0; i--) {
|
|||
|
|
if (elements[i].VisualColumn < visualColumn)
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
// search last element that has a caret stop
|
|||
|
|
for (; i >= 0; i--) {
|
|||
|
|
int pos = elements[i].GetNextCaretPosition(
|
|||
|
|
Math.Min(visualColumn, elements[i].VisualColumn + elements[i].VisualLength + 1),
|
|||
|
|
direction, mode);
|
|||
|
|
if (pos >= 0)
|
|||
|
|
return pos;
|
|||
|
|
}
|
|||
|
|
// If we've found nothing, and the first element doesn't handle line borders,
|
|||
|
|
// return the line start as normal caret stop.
|
|||
|
|
if (visualColumn > 0 && !elements[0].HandlesLineBorders && HasImplicitStopAtLineStart(mode))
|
|||
|
|
return 0;
|
|||
|
|
} else {
|
|||
|
|
// Search Forwards:
|
|||
|
|
// If the first element doesn't handle line borders, return the line start as caret stop
|
|||
|
|
if (visualColumn < 0 && !elements[0].HandlesLineBorders && HasImplicitStopAtLineStart(mode))
|
|||
|
|
return 0;
|
|||
|
|
// skip elements that end before or at visualColumn
|
|||
|
|
for (i = 0; i < elements.Count; i++) {
|
|||
|
|
if (elements[i].VisualColumn + elements[i].VisualLength > visualColumn)
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
// search first element that has a caret stop
|
|||
|
|
for (; i < elements.Count; i++) {
|
|||
|
|
int pos = elements[i].GetNextCaretPosition(
|
|||
|
|
Math.Max(visualColumn, elements[i].VisualColumn - 1),
|
|||
|
|
direction, mode);
|
|||
|
|
if (pos >= 0)
|
|||
|
|
return pos;
|
|||
|
|
}
|
|||
|
|
// if we've found nothing, and the last element doesn't handle line borders,
|
|||
|
|
// return the line end as caret stop
|
|||
|
|
if ((allowVirtualSpace || !elements[elements.Count-1].HandlesLineBorders) && HasImplicitStopAtLineEnd(mode)) {
|
|||
|
|
if (visualColumn < this.VisualLength)
|
|||
|
|
return this.VisualLength;
|
|||
|
|
else if (allowVirtualSpace)
|
|||
|
|
return visualColumn + 1;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// we've found nothing, return -1 and let the caret search continue in the next line
|
|||
|
|
return -1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static bool HasStopsInVirtualSpace(CaretPositioningMode mode)
|
|||
|
|
{
|
|||
|
|
return mode == CaretPositioningMode.Normal;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static bool HasImplicitStopAtLineStart(CaretPositioningMode mode)
|
|||
|
|
{
|
|||
|
|
return mode == CaretPositioningMode.Normal;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "mode",
|
|||
|
|
Justification = "make method consistent with HasImplicitStopAtLineStart; might depend on mode in the future")]
|
|||
|
|
static bool HasImplicitStopAtLineEnd(CaretPositioningMode mode)
|
|||
|
|
{
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
VisualLineDrawingVisual visual;
|
|||
|
|
|
|||
|
|
internal VisualLineDrawingVisual Render()
|
|||
|
|
{
|
|||
|
|
Debug.Assert(phase == LifetimePhase.Live);
|
|||
|
|
if (visual == null)
|
|||
|
|
visual = new VisualLineDrawingVisual(this);
|
|||
|
|
return visual;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
sealed class VisualLineDrawingVisual : DrawingVisual
|
|||
|
|
{
|
|||
|
|
public readonly VisualLine VisualLine;
|
|||
|
|
public readonly double Height;
|
|||
|
|
internal bool IsAdded;
|
|||
|
|
|
|||
|
|
public VisualLineDrawingVisual(VisualLine visualLine)
|
|||
|
|
{
|
|||
|
|
this.VisualLine = visualLine;
|
|||
|
|
var drawingContext = RenderOpen();
|
|||
|
|
double pos = 0;
|
|||
|
|
foreach (TextLine textLine in visualLine.TextLines) {
|
|||
|
|
textLine.Draw(drawingContext, new Point(0, pos), InvertAxes.None);
|
|||
|
|
pos += textLine.Height;
|
|||
|
|
}
|
|||
|
|
this.Height = pos;
|
|||
|
|
drawingContext.Close();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
protected override GeometryHitTestResult HitTestCore(GeometryHitTestParameters hitTestParameters)
|
|||
|
|
{
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
|
|||
|
|
{
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|