Free cookie consent management tool by TermsFeed Policy Generator

source: branches/RefactorPluginInfrastructure-2522/HeuristicLab.ExtLibs/HeuristicLab.AvalonEdit/5.0.1/AvalonEdit-5.0.1/Editing/Caret.cs @ 13332

Last change on this file since 13332 was 11700, checked in by jkarder, 10 years ago

#2077: created branch and added first version

File size: 16.8 KB
Line 
1// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy of this
4// software and associated documentation files (the "Software"), to deal in the Software
5// without restriction, including without limitation the rights to use, copy, modify, merge,
6// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
7// to whom the Software is furnished to do so, subject to the following conditions:
8//
9// The above copyright notice and this permission notice shall be included in all copies or
10// substantial portions of the Software.
11//
12// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
14// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
15// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
17// DEALINGS IN THE SOFTWARE.
18
19using System;
20using System.Diagnostics;
21using System.Linq;
22using System.Windows;
23using System.Windows.Documents;
24using System.Windows.Media;
25using System.Windows.Media.TextFormatting;
26using System.Windows.Threading;
27using ICSharpCode.AvalonEdit.Document;
28using ICSharpCode.AvalonEdit.Rendering;
29using ICSharpCode.AvalonEdit.Utils;
30#if NREFACTORY
31using ICSharpCode.NRefactory;
32using ICSharpCode.NRefactory.Editor;
33#endif
34
35namespace ICSharpCode.AvalonEdit.Editing
36{
37  /// <summary>
38  /// Helper class with caret-related methods.
39  /// </summary>
40  public sealed class Caret
41  {
42    readonly TextArea textArea;
43    readonly TextView textView;
44    readonly CaretLayer caretAdorner;
45    bool visible;
46   
47    internal Caret(TextArea textArea)
48    {
49      this.textArea = textArea;
50      this.textView = textArea.TextView;
51      position = new TextViewPosition(1, 1, 0);
52     
53      caretAdorner = new CaretLayer(textArea);
54      textView.InsertLayer(caretAdorner, KnownLayer.Caret, LayerInsertionPosition.Replace);
55      textView.VisualLinesChanged += TextView_VisualLinesChanged;
56      textView.ScrollOffsetChanged += TextView_ScrollOffsetChanged;
57    }
58   
59    internal void UpdateIfVisible()
60    {
61      if (visible) {
62        Show();
63      }
64    }
65   
66    void TextView_VisualLinesChanged(object sender, EventArgs e)
67    {
68      if (visible) {
69        Show();
70      }
71      // required because the visual columns might have changed if the
72      // element generators did something differently than on the last run
73      // (e.g. a FoldingSection was collapsed)
74      InvalidateVisualColumn();
75    }
76   
77    void TextView_ScrollOffsetChanged(object sender, EventArgs e)
78    {
79      if (caretAdorner != null) {
80        caretAdorner.InvalidateVisual();
81      }
82    }
83   
84    double desiredXPos = double.NaN;
85    TextViewPosition position;
86   
87    /// <summary>
88    /// Gets/Sets the position of the caret.
89    /// Retrieving this property will validate the visual column (which can be expensive).
90    /// Use the <see cref="Location"/> property instead if you don't need the visual column.
91    /// </summary>
92    public TextViewPosition Position {
93      get {
94        ValidateVisualColumn();
95        return position;
96      }
97      set {
98        if (position != value) {
99          position = value;
100         
101          storedCaretOffset = -1;
102         
103          //Debug.WriteLine("Caret position changing to " + value);
104         
105          ValidatePosition();
106          InvalidateVisualColumn();
107          RaisePositionChanged();
108          Log("Caret position changed to " + value);
109          if (visible)
110            Show();
111        }
112      }
113    }
114   
115    /// <summary>
116    /// Gets the caret position without validating it.
117    /// </summary>
118    internal TextViewPosition NonValidatedPosition {
119      get {
120        return position;
121      }
122    }
123   
124    /// <summary>
125    /// Gets/Sets the location of the caret.
126    /// The getter of this property is faster than <see cref="Position"/> because it doesn't have
127    /// to validate the visual column.
128    /// </summary>
129    public TextLocation Location {
130      get {
131        return position.Location;
132      }
133      set {
134        this.Position = new TextViewPosition(value);
135      }
136    }
137   
138    /// <summary>
139    /// Gets/Sets the caret line.
140    /// </summary>
141    public int Line {
142      get { return position.Line; }
143      set {
144        this.Position = new TextViewPosition(value, position.Column);
145      }
146    }
147   
148    /// <summary>
149    /// Gets/Sets the caret column.
150    /// </summary>
151    public int Column {
152      get { return position.Column; }
153      set {
154        this.Position = new TextViewPosition(position.Line, value);
155      }
156    }
157   
158    /// <summary>
159    /// Gets/Sets the caret visual column.
160    /// </summary>
161    public int VisualColumn {
162      get {
163        ValidateVisualColumn();
164        return position.VisualColumn;
165      }
166      set {
167        this.Position = new TextViewPosition(position.Line, position.Column, value);
168      }
169    }
170   
171    bool isInVirtualSpace;
172   
173    /// <summary>
174    /// Gets whether the caret is in virtual space.
175    /// </summary>
176    public bool IsInVirtualSpace {
177      get {
178        ValidateVisualColumn();
179        return isInVirtualSpace;
180      }
181    }
182   
183    int storedCaretOffset;
184   
185    internal void OnDocumentChanging()
186    {
187      storedCaretOffset = this.Offset;
188      InvalidateVisualColumn();
189    }
190   
191    internal void OnDocumentChanged(DocumentChangeEventArgs e)
192    {
193      InvalidateVisualColumn();
194      if (storedCaretOffset >= 0) {
195        // If the caret is at the end of a selection, we don't expand the selection if something
196        // is inserted at the end. Thus we also need to keep the caret in front of the insertion.
197        AnchorMovementType caretMovementType;
198        if (!textArea.Selection.IsEmpty && storedCaretOffset == textArea.Selection.SurroundingSegment.EndOffset)
199          caretMovementType = AnchorMovementType.BeforeInsertion;
200        else
201          caretMovementType = AnchorMovementType.Default;
202        int newCaretOffset = e.GetNewOffset(storedCaretOffset, caretMovementType);
203        TextDocument document = textArea.Document;
204        if (document != null) {
205          // keep visual column
206          this.Position = new TextViewPosition(document.GetLocation(newCaretOffset), position.VisualColumn);
207        }
208      }
209      storedCaretOffset = -1;
210    }
211   
212    /// <summary>
213    /// Gets/Sets the caret offset.
214    /// Setting the caret offset has the side effect of setting the <see cref="DesiredXPos"/> to NaN.
215    /// </summary>
216    public int Offset {
217      get {
218        TextDocument document = textArea.Document;
219        if (document == null) {
220          return 0;
221        } else {
222          return document.GetOffset(position.Location);
223        }
224      }
225      set {
226        TextDocument document = textArea.Document;
227        if (document != null) {
228          this.Position = new TextViewPosition(document.GetLocation(value));
229          this.DesiredXPos = double.NaN;
230        }
231      }
232    }
233   
234    /// <summary>
235    /// Gets/Sets the desired x-position of the caret, in device-independent pixels.
236    /// This property is NaN if the caret has no desired position.
237    /// </summary>
238    public double DesiredXPos {
239      get { return desiredXPos; }
240      set { desiredXPos = value; }
241    }
242   
243    void ValidatePosition()
244    {
245      if (position.Line < 1)
246        position.Line = 1;
247      if (position.Column < 1)
248        position.Column = 1;
249      if (position.VisualColumn < -1)
250        position.VisualColumn = -1;
251      TextDocument document = textArea.Document;
252      if (document != null) {
253        if (position.Line > document.LineCount) {
254          position.Line = document.LineCount;
255          position.Column = document.GetLineByNumber(position.Line).Length + 1;
256          position.VisualColumn = -1;
257        } else {
258          DocumentLine line = document.GetLineByNumber(position.Line);
259          if (position.Column > line.Length + 1) {
260            position.Column = line.Length + 1;
261            position.VisualColumn = -1;
262          }
263        }
264      }
265    }
266   
267    /// <summary>
268    /// Event raised when the caret position has changed.
269    /// If the caret position is changed inside a document update (between BeginUpdate/EndUpdate calls),
270    /// the PositionChanged event is raised only once at the end of the document update.
271    /// </summary>
272    public event EventHandler PositionChanged;
273   
274    bool raisePositionChangedOnUpdateFinished;
275   
276    void RaisePositionChanged()
277    {
278      if (textArea.Document != null && textArea.Document.IsInUpdate) {
279        raisePositionChangedOnUpdateFinished = true;
280      } else {
281        if (PositionChanged != null) {
282          PositionChanged(this, EventArgs.Empty);
283        }
284      }
285    }
286   
287    internal void OnDocumentUpdateFinished()
288    {
289      if (raisePositionChangedOnUpdateFinished) {
290        if (PositionChanged != null) {
291          PositionChanged(this, EventArgs.Empty);
292        }
293      }
294    }
295   
296    bool visualColumnValid;
297   
298    void ValidateVisualColumn()
299    {
300      if (!visualColumnValid) {
301        TextDocument document = textArea.Document;
302        if (document != null) {
303          Debug.WriteLine("Explicit validation of caret column");
304          var documentLine = document.GetLineByNumber(position.Line);
305          RevalidateVisualColumn(textView.GetOrConstructVisualLine(documentLine));
306        }
307      }
308    }
309   
310    void InvalidateVisualColumn()
311    {
312      visualColumnValid = false;
313    }
314   
315    /// <summary>
316    /// Validates the visual column of the caret using the specified visual line.
317    /// The visual line must contain the caret offset.
318    /// </summary>
319    void RevalidateVisualColumn(VisualLine visualLine)
320    {
321      if (visualLine == null)
322        throw new ArgumentNullException("visualLine");
323     
324      // mark column as validated
325      visualColumnValid = true;
326     
327      int caretOffset = textView.Document.GetOffset(position.Location);
328      int firstDocumentLineOffset = visualLine.FirstDocumentLine.Offset;
329      position.VisualColumn = visualLine.ValidateVisualColumn(position, textArea.Selection.EnableVirtualSpace);
330     
331      // search possible caret positions
332      int newVisualColumnForwards = visualLine.GetNextCaretPosition(position.VisualColumn - 1, LogicalDirection.Forward, CaretPositioningMode.Normal, textArea.Selection.EnableVirtualSpace);
333      // If position.VisualColumn was valid, we're done with validation.
334      if (newVisualColumnForwards != position.VisualColumn) {
335        // also search backwards so that we can pick the better match
336        int newVisualColumnBackwards = visualLine.GetNextCaretPosition(position.VisualColumn + 1, LogicalDirection.Backward, CaretPositioningMode.Normal, textArea.Selection.EnableVirtualSpace);
337       
338        if (newVisualColumnForwards < 0 && newVisualColumnBackwards < 0)
339          throw ThrowUtil.NoValidCaretPosition();
340       
341        // determine offsets for new visual column positions
342        int newOffsetForwards;
343        if (newVisualColumnForwards >= 0)
344          newOffsetForwards = visualLine.GetRelativeOffset(newVisualColumnForwards) + firstDocumentLineOffset;
345        else
346          newOffsetForwards = -1;
347        int newOffsetBackwards;
348        if (newVisualColumnBackwards >= 0)
349          newOffsetBackwards = visualLine.GetRelativeOffset(newVisualColumnBackwards) + firstDocumentLineOffset;
350        else
351          newOffsetBackwards = -1;
352       
353        int newVisualColumn, newOffset;
354        // if there's only one valid position, use it
355        if (newVisualColumnForwards < 0) {
356          newVisualColumn = newVisualColumnBackwards;
357          newOffset = newOffsetBackwards;
358        } else if (newVisualColumnBackwards < 0) {
359          newVisualColumn = newVisualColumnForwards;
360          newOffset = newOffsetForwards;
361        } else {
362          // two valid positions: find the better match
363          if (Math.Abs(newOffsetBackwards - caretOffset) < Math.Abs(newOffsetForwards - caretOffset)) {
364            // backwards is better
365            newVisualColumn = newVisualColumnBackwards;
366            newOffset = newOffsetBackwards;
367          } else {
368            // forwards is better
369            newVisualColumn = newVisualColumnForwards;
370            newOffset = newOffsetForwards;
371          }
372        }
373        this.Position = new TextViewPosition(textView.Document.GetLocation(newOffset), newVisualColumn);
374      }
375      isInVirtualSpace = (position.VisualColumn > visualLine.VisualLength);
376    }
377   
378    Rect CalcCaretRectangle(VisualLine visualLine)
379    {
380      if (!visualColumnValid) {
381        RevalidateVisualColumn(visualLine);
382      }
383     
384      TextLine textLine = visualLine.GetTextLine(position.VisualColumn, position.IsAtEndOfLine);
385      double xPos = visualLine.GetTextLineVisualXPosition(textLine, position.VisualColumn);
386      double lineTop = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextTop);
387      double lineBottom = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextBottom);
388     
389      return new Rect(xPos,
390                      lineTop,
391                      SystemParameters.CaretWidth,
392                      lineBottom - lineTop);
393    }
394   
395    Rect CalcCaretOverstrikeRectangle(VisualLine visualLine)
396    {
397      if (!visualColumnValid) {
398        RevalidateVisualColumn(visualLine);
399      }
400     
401      int currentPos = position.VisualColumn;
402      // The text being overwritten in overstrike mode is everything up to the next normal caret stop
403      int nextPos = visualLine.GetNextCaretPosition(currentPos, LogicalDirection.Forward, CaretPositioningMode.Normal, true);
404      TextLine textLine = visualLine.GetTextLine(currentPos);
405     
406      Rect r;
407      if (currentPos < visualLine.VisualLength) {
408        // If the caret is within the text, use GetTextBounds() for the text being overwritten.
409        // This is necessary to ensure the rectangle is calculated correctly in bidirectional text.
410        var textBounds = textLine.GetTextBounds(currentPos, nextPos - currentPos)[0];
411        r = textBounds.Rectangle;
412        r.Y += visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.LineTop);
413      } else {
414        // If the caret is at the end of the line (or in virtual space),
415        // use the visual X position of currentPos and nextPos (one or more of which will be in virtual space)
416        double xPos = visualLine.GetTextLineVisualXPosition(textLine, currentPos);
417        double xPos2 = visualLine.GetTextLineVisualXPosition(textLine, nextPos);
418        double lineTop = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextTop);
419        double lineBottom = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextBottom);
420        r = new Rect(xPos, lineTop, xPos2 - xPos, lineBottom - lineTop);
421      }
422      // If the caret is too small (e.g. in front of zero-width character), ensure it's still visible
423      if (r.Width < SystemParameters.CaretWidth)
424        r.Width = SystemParameters.CaretWidth;
425      return r;
426    }
427   
428    /// <summary>
429    /// Returns the caret rectangle. The coordinate system is in device-independent pixels from the top of the document.
430    /// </summary>
431    public Rect CalculateCaretRectangle()
432    {
433      if (textView != null && textView.Document != null) {
434        VisualLine visualLine = textView.GetOrConstructVisualLine(textView.Document.GetLineByNumber(position.Line));
435        return textArea.OverstrikeMode ? CalcCaretOverstrikeRectangle(visualLine) : CalcCaretRectangle(visualLine);
436      } else {
437        return Rect.Empty;
438      }
439    }
440   
441    /// <summary>
442    /// Minimum distance of the caret to the view border.
443    /// </summary>
444    internal const double MinimumDistanceToViewBorder = 30;
445   
446    /// <summary>
447    /// Scrolls the text view so that the caret is visible.
448    /// </summary>
449    public void BringCaretToView()
450    {
451      BringCaretToView(MinimumDistanceToViewBorder);
452    }
453   
454    internal void BringCaretToView(double border)
455    {
456      Rect caretRectangle = CalculateCaretRectangle();
457      if (!caretRectangle.IsEmpty) {
458        caretRectangle.Inflate(border, border);
459        textView.MakeVisible(caretRectangle);
460      }
461    }
462   
463    /// <summary>
464    /// Makes the caret visible and updates its on-screen position.
465    /// </summary>
466    public void Show()
467    {
468      Log("Caret.Show()");
469      visible = true;
470      if (!showScheduled) {
471        showScheduled = true;
472        textArea.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(ShowInternal));
473      }
474    }
475   
476    bool showScheduled;
477    bool hasWin32Caret;
478   
479    void ShowInternal()
480    {
481      showScheduled = false;
482     
483      // if show was scheduled but caret hidden in the meantime
484      if (!visible)
485        return;
486     
487      if (caretAdorner != null && textView != null) {
488        VisualLine visualLine = textView.GetVisualLine(position.Line);
489        if (visualLine != null) {
490          Rect caretRect = this.textArea.OverstrikeMode ? CalcCaretOverstrikeRectangle(visualLine) : CalcCaretRectangle(visualLine);
491          // Create Win32 caret so that Windows knows where our managed caret is. This is necessary for
492          // features like 'Follow text editing' in the Windows Magnifier.
493          if (!hasWin32Caret) {
494            hasWin32Caret = Win32.CreateCaret(textView, caretRect.Size);
495          }
496          if (hasWin32Caret) {
497            Win32.SetCaretPosition(textView, caretRect.Location - textView.ScrollOffset);
498          }
499          caretAdorner.Show(caretRect);
500          textArea.ime.UpdateCompositionWindow();
501        } else {
502          caretAdorner.Hide();
503        }
504      }
505    }
506   
507    /// <summary>
508    /// Makes the caret invisible.
509    /// </summary>
510    public void Hide()
511    {
512      Log("Caret.Hide()");
513      visible = false;
514      if (hasWin32Caret) {
515        Win32.DestroyCaret();
516        hasWin32Caret = false;
517      }
518      if (caretAdorner != null) {
519        caretAdorner.Hide();
520      }
521    }
522   
523    [Conditional("DEBUG")]
524    static void Log(string text)
525    {
526      // commented out to make debug output less noisy - add back if there are any problems with the caret
527      //Debug.WriteLine(text);
528    }
529   
530    /// <summary>
531    /// Gets/Sets the color of the caret.
532    /// </summary>
533    public Brush CaretBrush {
534      get { return caretAdorner.CaretBrush; }
535      set { caretAdorner.CaretBrush = value; }
536    }
537  }
538}
Note: See TracBrowser for help on using the repository browser.