// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team // // Permission is hereby granted, free of charge, to any person obtaining a copy of this // software and associated documentation files (the "Software"), to deal in the Software // without restriction, including without limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons // to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Diagnostics; using System.Linq; using System.Windows; using System.Windows.Documents; using System.Windows.Media; using System.Windows.Media.TextFormatting; using System.Windows.Threading; using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Rendering; using ICSharpCode.AvalonEdit.Utils; #if NREFACTORY using ICSharpCode.NRefactory; using ICSharpCode.NRefactory.Editor; #endif namespace ICSharpCode.AvalonEdit.Editing { /// /// Helper class with caret-related methods. /// public sealed class Caret { readonly TextArea textArea; readonly TextView textView; readonly CaretLayer caretAdorner; bool visible; internal Caret(TextArea textArea) { this.textArea = textArea; this.textView = textArea.TextView; position = new TextViewPosition(1, 1, 0); caretAdorner = new CaretLayer(textArea); textView.InsertLayer(caretAdorner, KnownLayer.Caret, LayerInsertionPosition.Replace); textView.VisualLinesChanged += TextView_VisualLinesChanged; textView.ScrollOffsetChanged += TextView_ScrollOffsetChanged; } internal void UpdateIfVisible() { if (visible) { Show(); } } void TextView_VisualLinesChanged(object sender, EventArgs e) { if (visible) { Show(); } // required because the visual columns might have changed if the // element generators did something differently than on the last run // (e.g. a FoldingSection was collapsed) InvalidateVisualColumn(); } void TextView_ScrollOffsetChanged(object sender, EventArgs e) { if (caretAdorner != null) { caretAdorner.InvalidateVisual(); } } double desiredXPos = double.NaN; TextViewPosition position; /// /// Gets/Sets the position of the caret. /// Retrieving this property will validate the visual column (which can be expensive). /// Use the property instead if you don't need the visual column. /// public TextViewPosition Position { get { ValidateVisualColumn(); return position; } set { if (position != value) { position = value; storedCaretOffset = -1; //Debug.WriteLine("Caret position changing to " + value); ValidatePosition(); InvalidateVisualColumn(); RaisePositionChanged(); Log("Caret position changed to " + value); if (visible) Show(); } } } /// /// Gets the caret position without validating it. /// internal TextViewPosition NonValidatedPosition { get { return position; } } /// /// Gets/Sets the location of the caret. /// The getter of this property is faster than because it doesn't have /// to validate the visual column. /// public TextLocation Location { get { return position.Location; } set { this.Position = new TextViewPosition(value); } } /// /// Gets/Sets the caret line. /// public int Line { get { return position.Line; } set { this.Position = new TextViewPosition(value, position.Column); } } /// /// Gets/Sets the caret column. /// public int Column { get { return position.Column; } set { this.Position = new TextViewPosition(position.Line, value); } } /// /// Gets/Sets the caret visual column. /// public int VisualColumn { get { ValidateVisualColumn(); return position.VisualColumn; } set { this.Position = new TextViewPosition(position.Line, position.Column, value); } } bool isInVirtualSpace; /// /// Gets whether the caret is in virtual space. /// public bool IsInVirtualSpace { get { ValidateVisualColumn(); return isInVirtualSpace; } } int storedCaretOffset; internal void OnDocumentChanging() { storedCaretOffset = this.Offset; InvalidateVisualColumn(); } internal void OnDocumentChanged(DocumentChangeEventArgs e) { InvalidateVisualColumn(); if (storedCaretOffset >= 0) { // If the caret is at the end of a selection, we don't expand the selection if something // is inserted at the end. Thus we also need to keep the caret in front of the insertion. AnchorMovementType caretMovementType; if (!textArea.Selection.IsEmpty && storedCaretOffset == textArea.Selection.SurroundingSegment.EndOffset) caretMovementType = AnchorMovementType.BeforeInsertion; else caretMovementType = AnchorMovementType.Default; int newCaretOffset = e.GetNewOffset(storedCaretOffset, caretMovementType); TextDocument document = textArea.Document; if (document != null) { // keep visual column this.Position = new TextViewPosition(document.GetLocation(newCaretOffset), position.VisualColumn); } } storedCaretOffset = -1; } /// /// Gets/Sets the caret offset. /// Setting the caret offset has the side effect of setting the to NaN. /// public int Offset { get { TextDocument document = textArea.Document; if (document == null) { return 0; } else { return document.GetOffset(position.Location); } } set { TextDocument document = textArea.Document; if (document != null) { this.Position = new TextViewPosition(document.GetLocation(value)); this.DesiredXPos = double.NaN; } } } /// /// Gets/Sets the desired x-position of the caret, in device-independent pixels. /// This property is NaN if the caret has no desired position. /// public double DesiredXPos { get { return desiredXPos; } set { desiredXPos = value; } } void ValidatePosition() { if (position.Line < 1) position.Line = 1; if (position.Column < 1) position.Column = 1; if (position.VisualColumn < -1) position.VisualColumn = -1; TextDocument document = textArea.Document; if (document != null) { if (position.Line > document.LineCount) { position.Line = document.LineCount; position.Column = document.GetLineByNumber(position.Line).Length + 1; position.VisualColumn = -1; } else { DocumentLine line = document.GetLineByNumber(position.Line); if (position.Column > line.Length + 1) { position.Column = line.Length + 1; position.VisualColumn = -1; } } } } /// /// Event raised when the caret position has changed. /// If the caret position is changed inside a document update (between BeginUpdate/EndUpdate calls), /// the PositionChanged event is raised only once at the end of the document update. /// public event EventHandler PositionChanged; bool raisePositionChangedOnUpdateFinished; void RaisePositionChanged() { if (textArea.Document != null && textArea.Document.IsInUpdate) { raisePositionChangedOnUpdateFinished = true; } else { if (PositionChanged != null) { PositionChanged(this, EventArgs.Empty); } } } internal void OnDocumentUpdateFinished() { if (raisePositionChangedOnUpdateFinished) { if (PositionChanged != null) { PositionChanged(this, EventArgs.Empty); } } } bool visualColumnValid; void ValidateVisualColumn() { if (!visualColumnValid) { TextDocument document = textArea.Document; if (document != null) { Debug.WriteLine("Explicit validation of caret column"); var documentLine = document.GetLineByNumber(position.Line); RevalidateVisualColumn(textView.GetOrConstructVisualLine(documentLine)); } } } void InvalidateVisualColumn() { visualColumnValid = false; } /// /// Validates the visual column of the caret using the specified visual line. /// The visual line must contain the caret offset. /// void RevalidateVisualColumn(VisualLine visualLine) { if (visualLine == null) throw new ArgumentNullException("visualLine"); // mark column as validated visualColumnValid = true; int caretOffset = textView.Document.GetOffset(position.Location); int firstDocumentLineOffset = visualLine.FirstDocumentLine.Offset; position.VisualColumn = visualLine.ValidateVisualColumn(position, textArea.Selection.EnableVirtualSpace); // search possible caret positions int newVisualColumnForwards = visualLine.GetNextCaretPosition(position.VisualColumn - 1, LogicalDirection.Forward, CaretPositioningMode.Normal, textArea.Selection.EnableVirtualSpace); // If position.VisualColumn was valid, we're done with validation. if (newVisualColumnForwards != position.VisualColumn) { // also search backwards so that we can pick the better match int newVisualColumnBackwards = visualLine.GetNextCaretPosition(position.VisualColumn + 1, LogicalDirection.Backward, CaretPositioningMode.Normal, textArea.Selection.EnableVirtualSpace); if (newVisualColumnForwards < 0 && newVisualColumnBackwards < 0) throw ThrowUtil.NoValidCaretPosition(); // determine offsets for new visual column positions int newOffsetForwards; if (newVisualColumnForwards >= 0) newOffsetForwards = visualLine.GetRelativeOffset(newVisualColumnForwards) + firstDocumentLineOffset; else newOffsetForwards = -1; int newOffsetBackwards; if (newVisualColumnBackwards >= 0) newOffsetBackwards = visualLine.GetRelativeOffset(newVisualColumnBackwards) + firstDocumentLineOffset; else newOffsetBackwards = -1; int newVisualColumn, newOffset; // if there's only one valid position, use it if (newVisualColumnForwards < 0) { newVisualColumn = newVisualColumnBackwards; newOffset = newOffsetBackwards; } else if (newVisualColumnBackwards < 0) { newVisualColumn = newVisualColumnForwards; newOffset = newOffsetForwards; } else { // two valid positions: find the better match if (Math.Abs(newOffsetBackwards - caretOffset) < Math.Abs(newOffsetForwards - caretOffset)) { // backwards is better newVisualColumn = newVisualColumnBackwards; newOffset = newOffsetBackwards; } else { // forwards is better newVisualColumn = newVisualColumnForwards; newOffset = newOffsetForwards; } } this.Position = new TextViewPosition(textView.Document.GetLocation(newOffset), newVisualColumn); } isInVirtualSpace = (position.VisualColumn > visualLine.VisualLength); } Rect CalcCaretRectangle(VisualLine visualLine) { if (!visualColumnValid) { RevalidateVisualColumn(visualLine); } TextLine textLine = visualLine.GetTextLine(position.VisualColumn, position.IsAtEndOfLine); double xPos = visualLine.GetTextLineVisualXPosition(textLine, position.VisualColumn); double lineTop = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextTop); double lineBottom = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextBottom); return new Rect(xPos, lineTop, SystemParameters.CaretWidth, lineBottom - lineTop); } Rect CalcCaretOverstrikeRectangle(VisualLine visualLine) { if (!visualColumnValid) { RevalidateVisualColumn(visualLine); } int currentPos = position.VisualColumn; // The text being overwritten in overstrike mode is everything up to the next normal caret stop int nextPos = visualLine.GetNextCaretPosition(currentPos, LogicalDirection.Forward, CaretPositioningMode.Normal, true); TextLine textLine = visualLine.GetTextLine(currentPos); Rect r; if (currentPos < visualLine.VisualLength) { // If the caret is within the text, use GetTextBounds() for the text being overwritten. // This is necessary to ensure the rectangle is calculated correctly in bidirectional text. var textBounds = textLine.GetTextBounds(currentPos, nextPos - currentPos)[0]; r = textBounds.Rectangle; r.Y += visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.LineTop); } else { // If the caret is at the end of the line (or in virtual space), // use the visual X position of currentPos and nextPos (one or more of which will be in virtual space) double xPos = visualLine.GetTextLineVisualXPosition(textLine, currentPos); double xPos2 = visualLine.GetTextLineVisualXPosition(textLine, nextPos); double lineTop = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextTop); double lineBottom = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextBottom); r = new Rect(xPos, lineTop, xPos2 - xPos, lineBottom - lineTop); } // If the caret is too small (e.g. in front of zero-width character), ensure it's still visible if (r.Width < SystemParameters.CaretWidth) r.Width = SystemParameters.CaretWidth; return r; } /// /// Returns the caret rectangle. The coordinate system is in device-independent pixels from the top of the document. /// public Rect CalculateCaretRectangle() { if (textView != null && textView.Document != null) { VisualLine visualLine = textView.GetOrConstructVisualLine(textView.Document.GetLineByNumber(position.Line)); return textArea.OverstrikeMode ? CalcCaretOverstrikeRectangle(visualLine) : CalcCaretRectangle(visualLine); } else { return Rect.Empty; } } /// /// Minimum distance of the caret to the view border. /// internal const double MinimumDistanceToViewBorder = 30; /// /// Scrolls the text view so that the caret is visible. /// public void BringCaretToView() { BringCaretToView(MinimumDistanceToViewBorder); } internal void BringCaretToView(double border) { Rect caretRectangle = CalculateCaretRectangle(); if (!caretRectangle.IsEmpty) { caretRectangle.Inflate(border, border); textView.MakeVisible(caretRectangle); } } /// /// Makes the caret visible and updates its on-screen position. /// public void Show() { Log("Caret.Show()"); visible = true; if (!showScheduled) { showScheduled = true; textArea.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(ShowInternal)); } } bool showScheduled; bool hasWin32Caret; void ShowInternal() { showScheduled = false; // if show was scheduled but caret hidden in the meantime if (!visible) return; if (caretAdorner != null && textView != null) { VisualLine visualLine = textView.GetVisualLine(position.Line); if (visualLine != null) { Rect caretRect = this.textArea.OverstrikeMode ? CalcCaretOverstrikeRectangle(visualLine) : CalcCaretRectangle(visualLine); // Create Win32 caret so that Windows knows where our managed caret is. This is necessary for // features like 'Follow text editing' in the Windows Magnifier. if (!hasWin32Caret) { hasWin32Caret = Win32.CreateCaret(textView, caretRect.Size); } if (hasWin32Caret) { Win32.SetCaretPosition(textView, caretRect.Location - textView.ScrollOffset); } caretAdorner.Show(caretRect); textArea.ime.UpdateCompositionWindow(); } else { caretAdorner.Hide(); } } } /// /// Makes the caret invisible. /// public void Hide() { Log("Caret.Hide()"); visible = false; if (hasWin32Caret) { Win32.DestroyCaret(); hasWin32Caret = false; } if (caretAdorner != null) { caretAdorner.Hide(); } } [Conditional("DEBUG")] static void Log(string text) { // commented out to make debug output less noisy - add back if there are any problems with the caret //Debug.WriteLine(text); } /// /// Gets/Sets the color of the caret. /// public Brush CaretBrush { get { return caretAdorner.CaretBrush; } set { caretAdorner.CaretBrush = value; } } } }