diff options
Diffstat (limited to 'Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLine.cs')
| -rw-r--r-- | Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLine.cs | 681 |
1 files changed, 681 insertions, 0 deletions
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLine.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLine.cs new file mode 100644 index 000000000..70727dd07 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLine.cs @@ -0,0 +1,681 @@ +// 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 Tango.Scripting.Editors.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 Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.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; + } + } +} |
