diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/Caret.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/Caret.cs index 98a2e67e6e..9ecb8f5281 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/Caret.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/Caret.cs @@ -289,7 +289,7 @@ namespace ICSharpCode.AvalonEdit.Editing if (offsetFromVisualColumn != caretOffset) { position.VisualColumn = visualLine.GetVisualColumn(caretOffset - firstDocumentLineOffset); } else { - if (position.VisualColumn > visualLine.VisualLength) { + if (position.VisualColumn > visualLine.VisualLength && !textArea.Options.EnableVirtualSpace) { position.VisualColumn = visualLine.VisualLength; } } @@ -347,7 +347,7 @@ namespace ICSharpCode.AvalonEdit.Editing } TextLine textLine = visualLine.GetTextLine(position.VisualColumn); - double xPos = textLine.GetDistanceFromCharacterHit(new CharacterHit(position.VisualColumn, 0)); + double xPos = visualLine.GetTextLineVisualXPosition(textLine, position.VisualColumn); double lineTop = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextTop); double lineBottom = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.LineBottom); diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/CaretNavigationCommandHandler.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/CaretNavigationCommandHandler.cs index 216a1e249d..c39a350623 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/CaretNavigationCommandHandler.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/CaretNavigationCommandHandler.cs @@ -4,10 +4,10 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Windows; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media.TextFormatting; - using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Rendering; using ICSharpCode.AvalonEdit.Utils; @@ -250,7 +250,7 @@ namespace ICSharpCode.AvalonEdit.Editing // moving up/down happens using the desired visual X position double xPos = textArea.Caret.DesiredXPos; if (double.IsNaN(xPos)) - xPos = textLine.GetDistanceFromCharacterHit(new CharacterHit(caretVisualColumn, 0)); + xPos = visualLine.GetTextLineVisualXPosition(textLine, caretVisualColumn); // now find the TextLine+VisualLine where the caret will end up in VisualLine targetVisualLine = visualLine; TextLine targetLine; @@ -306,8 +306,9 @@ namespace ICSharpCode.AvalonEdit.Editing throw new NotSupportedException(direction.ToString()); } if (targetLine != null) { - CharacterHit ch = targetLine.GetCharacterHitFromDistance(xPos); - SetCaretPosition(textArea, targetVisualLine, targetLine, ch, false); + double yPos = targetVisualLine.GetTextLineVisualYPosition(targetLine, VisualYPosition.LineMiddle); + int newVisualColumn = targetVisualLine.GetVisualColumn(new Point(xPos, yPos)); + SetCaretPosition(textArea, targetVisualLine, targetLine, newVisualColumn, false); textArea.Caret.DesiredXPos = xPos; } } @@ -315,12 +316,13 @@ namespace ICSharpCode.AvalonEdit.Editing #region SetCaretPosition static void SetCaretPosition(TextArea textArea, VisualLine targetVisualLine, TextLine targetLine, - CharacterHit ch, bool allowWrapToNextLine) + int newVisualColumn, bool allowWrapToNextLine) { - int newVisualColumn = ch.FirstCharacterIndex + ch.TrailingLength; int targetLineStartCol = targetVisualLine.GetTextLineVisualStartColumn(targetLine); - if (!allowWrapToNextLine && newVisualColumn >= targetLineStartCol + targetLine.Length) - newVisualColumn = targetLineStartCol + targetLine.Length - 1; + if (!allowWrapToNextLine && newVisualColumn >= targetLineStartCol + targetLine.Length) { + if (newVisualColumn <= targetVisualLine.VisualLength) + newVisualColumn = targetLineStartCol + targetLine.Length - 1; + } int newOffset = targetVisualLine.GetRelativeOffset(newVisualColumn) + targetVisualLine.FirstDocumentLine.Offset; SetCaretPosition(textArea, newVisualColumn, newOffset); } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/RectangleSelection.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/RectangleSelection.cs index e66a34a42a..76eac2e3b7 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/RectangleSelection.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/RectangleSelection.cs @@ -13,7 +13,7 @@ using ICSharpCode.AvalonEdit.Utils; namespace ICSharpCode.AvalonEdit.Editing { /// - /// Rectangular selection. + /// Rectangular selection ("box selection"). /// public sealed class RectangleSelection : Selection { diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/BackgroundGeometryBuilder.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/BackgroundGeometryBuilder.cs index 4cd7d63c13..367b12340a 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/BackgroundGeometryBuilder.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/BackgroundGeometryBuilder.cs @@ -129,7 +129,7 @@ namespace ICSharpCode.AvalonEdit.Rendering if (segmentStartVCInLine == segmentEndVCInLine) { // GetTextBounds crashes for length=0, so we'll handle this case with GetDistanceFromCharacterHit // We need to return a rectangle to ensure empty lines are still visible - double pos = line.GetDistanceFromCharacterHit(new CharacterHit(segmentStartVCInLine, 0)); + double pos = vl.GetTextLineVisualXPosition(line, segmentStartVCInLine); pos -= scrollOffset.X; // The following special cases are necessary to get rid of empty rectangles at the end of a TextLine if "Show Spaces" is active. // If not excluded once, the same rectangle is calculated (and added) twice (since the offset could be mapped to two visual positions; end/start of line), if there is no trailing whitespace. diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/TextView.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/TextView.cs index 58f1c059a1..b4ef080727 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/TextView.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/TextView.cs @@ -1360,7 +1360,7 @@ namespace ICSharpCode.AvalonEdit.Rendering 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 WideSpaceWidth { + internal double WideSpaceWidth { get { if (wideSpaceWidth == 0) { MeasureWideSpaceWidthAndDefaultLineHeight(); diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLine.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLine.cs index 2fc7b960f9..22309b5b8d 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLine.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLine.cs @@ -224,8 +224,9 @@ namespace ICSharpCode.AvalonEdit.Rendering /// public TextLine GetTextLine(int visualColumn) { - ThrowUtil.CheckInRangeInclusive(visualColumn, "visualColumn", 0, VisualLength); - if (visualColumn == VisualLength) + if (visualColumn < 0) + throw new ArgumentOutOfRangeException("visualColumn"); + if (visualColumn >= VisualLength) return TextLines[TextLines.Count - 1]; foreach (TextLine line in TextLines) { if (visualColumn < line.Length) @@ -307,18 +308,45 @@ namespace ICSharpCode.AvalonEdit.Rendering public Point GetVisualPosition(int visualColumn, VisualYPosition yPositionMode) { TextLine textLine = GetTextLine(visualColumn); - double xPos = textLine.GetDistanceFromCharacterHit(new CharacterHit(visualColumn, 0)); + double xPos = GetTextLineVisualXPosition(textLine, visualColumn); double yPos = GetTextLineVisualYPosition(textLine, yPositionMode); return new Point(xPos, yPos); } + /// + /// 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. + /// + public double GetTextLineVisualXPosition(TextLine textLine, int visualColumn) + { + if (textLine == null) + throw new ArgumentNullException("textLine"); + double xPos = textLine.GetDistanceFromCharacterHit( + new CharacterHit(Math.Min(visualColumn, VisualLength), 0)); + if (visualColumn > VisualLength) { + xPos += (visualColumn - VisualLength) * textView.WideSpaceWidth; + } + return xPos; + } + /// /// 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. /// public int GetVisualColumn(Point point) + { + return GetVisualColumn(point, textView.Options.EnableVirtualSpace); + } + + public int GetVisualColumn(Point point, bool allowVirtualSpace) { TextLine textLine = GetTextLineByVisualYPosition(point.Y); + if (point.X > textLine.WidthIncludingTrailingWhitespace) { + if (allowVirtualSpace && textLine == TextLines[TextLines.Count - 1]) { + int virtualX = (int)Math.Round((point.X - textLine.WidthIncludingTrailingWhitespace) / textView.WideSpaceWidth); + return VisualLength + virtualX; + } + } CharacterHit ch = textLine.GetCharacterHitFromDistance(point.X); return ch.FirstCharacterIndex + ch.TrailingLength; } @@ -328,13 +356,24 @@ namespace ICSharpCode.AvalonEdit.Rendering /// If the user clicks between two visual columns, returns the first of those columns. /// public int GetVisualColumnFloor(Point point) + { + return GetVisualColumnFloor(point, textView.Options.EnableVirtualSpace); + } + + public int GetVisualColumnFloor(Point point, bool allowVirtualSpace) { TextLine textLine = GetTextLineByVisualYPosition(point.Y); if (point.X > textLine.WidthIncludingTrailingWhitespace) { - // GetCharacterHitFromDistance returns a hit with FirstCharacterIndex=last character inline - // and TrailingLength=1 when clicking behind the line, so the floor function needs to handle this case - // specially end return the line's end column instead. - return GetTextLineVisualStartColumn(textLine) + textLine.Length; + if (allowVirtualSpace && textLine == TextLines[TextLines.Count - 1]) { + // clicking virtual space in the last line + int virtualX = (int)((point.X - textLine.WidthIncludingTrailingWhitespace) / textView.WideSpaceWidth); + return VisualLength + 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; @@ -352,14 +391,23 @@ namespace ICSharpCode.AvalonEdit.Rendering { if (elements.Count == 0) { // special handling for empty visual lines: - // 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; + if (textView.Options.EnableVirtualSpace) { + 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; @@ -368,7 +416,10 @@ namespace ICSharpCode.AvalonEdit.Rendering // 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)) { - return this.VisualLength; + if (textView.Options.EnableVirtualSpace) + return visualColumn - 1; + else + return this.VisualLength; } // skip elements that start after or at visualColumn for (i = elements.Count - 1; i >= 0; i--) { @@ -407,8 +458,12 @@ namespace ICSharpCode.AvalonEdit.Rendering } // if we've found nothing, and 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)) - return this.VisualLength; + if (!elements[elements.Count-1].HandlesLineBorders && HasImplicitStopAtLineEnd(mode)) { + if (visualColumn < this.VisualLength) + return this.VisualLength; + else if (textView.Options.EnableVirtualSpace) + return visualColumn + 1; + } } // we've found nothing, return -1 and let the caret search continue in the next line return -1; diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/TextEditorOptions.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/TextEditorOptions.cs index a2b708a1f6..35af3fba72 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/TextEditorOptions.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/TextEditorOptions.cs @@ -362,5 +362,24 @@ namespace ICSharpCode.AvalonEdit } } } + + bool enableVirtualSpace; + + /// + /// Gets/Sets whether the user can set the caret behind the line ending + /// (into "virtual space"). + /// Note that virtual space is always used (independent from this setting) + /// when doing rectangle selections. + /// + [DefaultValue(false)] + public virtual bool EnableVirtualSpace { + get { return enableVirtualSpace; } + set { + if (enableVirtualSpace != value) { + enableVirtualSpace = value; + OnPropertyChanged("EnableVirtualSpace"); + } + } + } } }