// 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.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.ComponentModel.Design; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.TextFormatting; using System.Windows.Threading; using ICSharpCode.NRefactory.Editor; using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Utils; namespace ICSharpCode.AvalonEdit.Rendering { /// /// A virtualizing panel producing+showing s for a . /// /// This is the heart of the text editor, this class controls the text rendering process. /// /// Taken as a standalone control, it's a text viewer without any editing capability. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "The user usually doesn't work with TextView but with TextEditor; and nulling the Document property is sufficient to dispose everything.")] public class TextView : FrameworkElement, IScrollInfo, IWeakEventListener, ITextEditorComponent, IServiceProvider { #region Constructor static TextView() { ClipToBoundsProperty.OverrideMetadata(typeof(TextView), new FrameworkPropertyMetadata(Boxes.True)); FocusableProperty.OverrideMetadata(typeof(TextView), new FrameworkPropertyMetadata(Boxes.False)); } ColumnRulerRenderer columnRulerRenderer; CurrentLineHighlightRenderer currentLineHighlighRenderer; /// /// Creates a new TextView instance. /// public TextView() { services.AddService(typeof(TextView), this); textLayer = new TextLayer(this); elementGenerators = new ObserveAddRemoveCollection(ElementGenerator_Added, ElementGenerator_Removed); lineTransformers = new ObserveAddRemoveCollection(LineTransformer_Added, LineTransformer_Removed); backgroundRenderers = new ObserveAddRemoveCollection(BackgroundRenderer_Added, BackgroundRenderer_Removed); columnRulerRenderer = new ColumnRulerRenderer(this); currentLineHighlighRenderer = new CurrentLineHighlightRenderer(this); this.Options = new TextEditorOptions(); Debug.Assert(singleCharacterElementGenerator != null); // assert that the option change created the builtin element generators layers = new LayerCollection(this); InsertLayer(textLayer, KnownLayer.Text, LayerInsertionPosition.Replace); this.hoverLogic = new MouseHoverLogic(this); this.hoverLogic.MouseHover += (sender, e) => RaiseHoverEventPair(e, PreviewMouseHoverEvent, MouseHoverEvent); this.hoverLogic.MouseHoverStopped += (sender, e) => RaiseHoverEventPair(e, PreviewMouseHoverStoppedEvent, MouseHoverStoppedEvent); } #endregion #region Document Property /// /// Document property. /// public static readonly DependencyProperty DocumentProperty = DependencyProperty.Register("Document", typeof(TextDocument), typeof(TextView), new FrameworkPropertyMetadata(OnDocumentChanged)); TextDocument document; HeightTree heightTree; /// /// Gets/Sets the document displayed by the text editor. /// public TextDocument Document { get { return (TextDocument)GetValue(DocumentProperty); } set { SetValue(DocumentProperty, value); } } static void OnDocumentChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e) { ((TextView)dp).OnDocumentChanged((TextDocument)e.OldValue, (TextDocument)e.NewValue); } internal double FontSize { get { return (double)GetValue(TextBlock.FontSizeProperty); } } /// /// Occurs when the document property has changed. /// public event EventHandler DocumentChanged; void OnDocumentChanged(TextDocument oldValue, TextDocument newValue) { if (oldValue != null) { heightTree.Dispose(); heightTree = null; formatter.Dispose(); formatter = null; cachedElements.Dispose(); cachedElements = null; TextDocumentWeakEventManager.Changing.RemoveListener(oldValue, this); } this.document = newValue; ClearScrollData(); ClearVisualLines(); if (newValue != null) { TextDocumentWeakEventManager.Changing.AddListener(newValue, this); formatter = TextFormatterFactory.Create(this); InvalidateDefaultTextMetrics(); // measuring DefaultLineHeight depends on formatter heightTree = new HeightTree(newValue, DefaultLineHeight); cachedElements = new TextViewCachedElements(); } InvalidateMeasure(DispatcherPriority.Normal); if (DocumentChanged != null) DocumentChanged(this, EventArgs.Empty); } /// /// Recreates the text formatter that is used internally /// by calling . /// void RecreateTextFormatter() { if (formatter != null) { formatter.Dispose(); formatter = TextFormatterFactory.Create(this); Redraw(); } } void RecreateCachedElements() { if (cachedElements != null) { cachedElements.Dispose(); cachedElements = new TextViewCachedElements(); } } /// protected virtual bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) { if (managerType == typeof(TextDocumentWeakEventManager.Changing)) { // TODO: put redraw into background so that other input events can be handled before the redraw. // Unfortunately the "easy" approach (just use DispatcherPriority.Background) here makes the editor twice as slow because // the caret position change forces an immediate redraw, and the text input then forces a background redraw. // When fixing this, make sure performance on the SharpDevelop "type text in C# comment" stress test doesn't get significantly worse. DocumentChangeEventArgs change = (DocumentChangeEventArgs)e; Redraw(change.Offset, change.RemovalLength, DispatcherPriority.Normal); return true; } else if (managerType == typeof(PropertyChangedWeakEventManager)) { OnOptionChanged((PropertyChangedEventArgs)e); return true; } return false; } bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) { return ReceiveWeakEvent(managerType, sender, e); } #endregion #region Options property /// /// Options property. /// public static readonly DependencyProperty OptionsProperty = DependencyProperty.Register("Options", typeof(TextEditorOptions), typeof(TextView), new FrameworkPropertyMetadata(OnOptionsChanged)); /// /// Gets/Sets the options used by the text editor. /// public TextEditorOptions Options { get { return (TextEditorOptions)GetValue(OptionsProperty); } set { SetValue(OptionsProperty, value); } } /// /// Occurs when a text editor option has changed. /// public event PropertyChangedEventHandler OptionChanged; /// /// Raises the event. /// protected virtual void OnOptionChanged(PropertyChangedEventArgs e) { if (OptionChanged != null) { OptionChanged(this, e); } if (Options.ShowColumnRuler) columnRulerRenderer.SetRuler(Options.ColumnRulerPosition, ColumnRulerPen); else columnRulerRenderer.SetRuler(-1, ColumnRulerPen); UpdateBuiltinElementGeneratorsFromOptions(); Redraw(); } static void OnOptionsChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e) { ((TextView)dp).OnOptionsChanged((TextEditorOptions)e.OldValue, (TextEditorOptions)e.NewValue); } void OnOptionsChanged(TextEditorOptions oldValue, TextEditorOptions newValue) { if (oldValue != null) { PropertyChangedWeakEventManager.RemoveListener(oldValue, this); } if (newValue != null) { PropertyChangedWeakEventManager.AddListener(newValue, this); } OnOptionChanged(new PropertyChangedEventArgs(null)); } #endregion #region ElementGenerators+LineTransformers Properties readonly ObserveAddRemoveCollection elementGenerators; /// /// Gets a collection where element generators can be registered. /// public IList ElementGenerators { get { return elementGenerators; } } void ElementGenerator_Added(VisualLineElementGenerator generator) { ConnectToTextView(generator); Redraw(); } void ElementGenerator_Removed(VisualLineElementGenerator generator) { DisconnectFromTextView(generator); Redraw(); } readonly ObserveAddRemoveCollection lineTransformers; /// /// Gets a collection where line transformers can be registered. /// public IList LineTransformers { get { return lineTransformers; } } void LineTransformer_Added(IVisualLineTransformer lineTransformer) { ConnectToTextView(lineTransformer); Redraw(); } void LineTransformer_Removed(IVisualLineTransformer lineTransformer) { DisconnectFromTextView(lineTransformer); Redraw(); } #endregion #region Builtin ElementGenerators // NewLineElementGenerator newLineElementGenerator; SingleCharacterElementGenerator singleCharacterElementGenerator; LinkElementGenerator linkElementGenerator; MailLinkElementGenerator mailLinkElementGenerator; void UpdateBuiltinElementGeneratorsFromOptions() { TextEditorOptions options = this.Options; // AddRemoveDefaultElementGeneratorOnDemand(ref newLineElementGenerator, options.ShowEndOfLine); AddRemoveDefaultElementGeneratorOnDemand(ref singleCharacterElementGenerator, options.ShowBoxForControlCharacters || options.ShowSpaces || options.ShowTabs); AddRemoveDefaultElementGeneratorOnDemand(ref linkElementGenerator, options.EnableHyperlinks); AddRemoveDefaultElementGeneratorOnDemand(ref mailLinkElementGenerator, options.EnableEmailHyperlinks); } void AddRemoveDefaultElementGeneratorOnDemand(ref T generator, bool demand) where T : VisualLineElementGenerator, IBuiltinElementGenerator, new() { bool hasGenerator = generator != null; if (hasGenerator != demand) { if (demand) { generator = new T(); this.ElementGenerators.Add(generator); } else { this.ElementGenerators.Remove(generator); generator = null; } } if (generator != null) generator.FetchOptions(this.Options); } #endregion #region Layers internal readonly TextLayer textLayer; readonly LayerCollection layers; /// /// Gets the list of layers displayed in the text view. /// public UIElementCollection Layers { get { return layers; } } sealed class LayerCollection : UIElementCollection { readonly TextView textView; public LayerCollection(TextView textView) : base(textView, textView) { this.textView = textView; } public override void Clear() { base.Clear(); textView.LayersChanged(); } public override int Add(UIElement element) { int r = base.Add(element); textView.LayersChanged(); return r; } public override void RemoveAt(int index) { base.RemoveAt(index); textView.LayersChanged(); } public override void RemoveRange(int index, int count) { base.RemoveRange(index, count); textView.LayersChanged(); } } void LayersChanged() { textLayer.index = layers.IndexOf(textLayer); } /// /// Inserts a new layer at a position specified relative to an existing layer. /// /// The new layer to insert. /// The existing layer /// Specifies whether the layer is inserted above,below, or replaces the referenced layer public void InsertLayer(UIElement layer, KnownLayer referencedLayer, LayerInsertionPosition position) { if (layer == null) throw new ArgumentNullException("layer"); if (!Enum.IsDefined(typeof(KnownLayer), referencedLayer)) throw new InvalidEnumArgumentException("referencedLayer", (int)referencedLayer, typeof(KnownLayer)); if (!Enum.IsDefined(typeof(LayerInsertionPosition), position)) throw new InvalidEnumArgumentException("position", (int)position, typeof(LayerInsertionPosition)); if (referencedLayer == KnownLayer.Background && position != LayerInsertionPosition.Above) throw new InvalidOperationException("Cannot replace or insert below the background layer."); LayerPosition newPosition = new LayerPosition(referencedLayer, position); LayerPosition.SetLayerPosition(layer, newPosition); for (int i = 0; i < layers.Count; i++) { LayerPosition p = LayerPosition.GetLayerPosition(layers[i]); if (p != null) { if (p.KnownLayer == referencedLayer && p.Position == LayerInsertionPosition.Replace) { // found the referenced layer switch (position) { case LayerInsertionPosition.Below: layers.Insert(i, layer); return; case LayerInsertionPosition.Above: layers.Insert(i + 1, layer); return; case LayerInsertionPosition.Replace: layers[i] = layer; return; } } else if (p.KnownLayer == referencedLayer && p.Position == LayerInsertionPosition.Above || p.KnownLayer > referencedLayer) { // we skipped the insertion position (referenced layer does not exist?) layers.Insert(i, layer); return; } } } // inserting after all existing layers: layers.Add(layer); } /// protected override int VisualChildrenCount { get { return layers.Count + inlineObjects.Count; } } /// protected override Visual GetVisualChild(int index) { int cut = textLayer.index + 1; if (index < cut) return layers[index]; else if (index < cut + inlineObjects.Count) return inlineObjects[index - cut].Element; else return layers[index - inlineObjects.Count]; } /// protected override System.Collections.IEnumerator LogicalChildren { get { return inlineObjects.Select(io => io.Element).Concat(layers.Cast()).GetEnumerator(); } } #endregion #region Inline object handling List inlineObjects = new List(); /// /// Adds a new inline object. /// internal void AddInlineObject(InlineObjectRun inlineObject) { Debug.Assert(inlineObject.VisualLine != null); // Remove inline object if its already added, can happen e.g. when recreating textrun for word-wrapping bool alreadyAdded = false; for (int i = 0; i < inlineObjects.Count; i++) { if (inlineObjects[i].Element == inlineObject.Element) { RemoveInlineObjectRun(inlineObjects[i], true); inlineObjects.RemoveAt(i); alreadyAdded = true; break; } } inlineObjects.Add(inlineObject); if (!alreadyAdded) { AddVisualChild(inlineObject.Element); } inlineObject.Element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); inlineObject.desiredSize = inlineObject.Element.DesiredSize; } void MeasureInlineObjects() { // As part of MeasureOverride(), re-measure the inline objects foreach (InlineObjectRun inlineObject in inlineObjects) { if (inlineObject.VisualLine.IsDisposed) { // Don't re-measure inline objects that are going to be removed anyways. // If the inline object will be reused in a different VisualLine, we'll measure it in the AddInlineObject() call. continue; } inlineObject.Element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); if (!inlineObject.Element.DesiredSize.IsClose(inlineObject.desiredSize)) { // the element changed size -> recreate its parent visual line inlineObject.desiredSize = inlineObject.Element.DesiredSize; if (allVisualLines.Remove(inlineObject.VisualLine)) { DisposeVisualLine(inlineObject.VisualLine); } } } } List visualLinesWithOutstandingInlineObjects = new List(); void RemoveInlineObjects(VisualLine visualLine) { // Delay removing inline objects: // A document change immediately invalidates affected visual lines, but it does not // cause an immediate redraw. // To prevent inline objects from flickering when they are recreated, we delay removing // inline objects until the next redraw. if (visualLine.hasInlineObjects) { visualLinesWithOutstandingInlineObjects.Add(visualLine); } } /// /// Remove the inline objects that were marked for removal. /// void RemoveInlineObjectsNow() { if (visualLinesWithOutstandingInlineObjects.Count == 0) return; inlineObjects.RemoveAll( ior => { if (visualLinesWithOutstandingInlineObjects.Contains(ior.VisualLine)) { RemoveInlineObjectRun(ior, false); return true; } return false; }); visualLinesWithOutstandingInlineObjects.Clear(); } // Remove InlineObjectRun.Element from TextLayer. // Caller of RemoveInlineObjectRun will remove it from inlineObjects collection. void RemoveInlineObjectRun(InlineObjectRun ior, bool keepElement) { if (!keepElement && ior.Element.IsKeyboardFocusWithin) { // When the inline element that has the focus is removed, WPF will reset the // focus to the main window without raising appropriate LostKeyboardFocus events. // To work around this, we manually set focus to the next focusable parent. UIElement element = this; while (element != null && !element.Focusable) { element = VisualTreeHelper.GetParent(element) as UIElement; } if (element != null) Keyboard.Focus(element); } ior.VisualLine = null; if (!keepElement) RemoveVisualChild(ior.Element); } #endregion #region Brushes /// /// NonPrintableCharacterBrush dependency property. /// public static readonly DependencyProperty NonPrintableCharacterBrushProperty = DependencyProperty.Register("NonPrintableCharacterBrush", typeof(Brush), typeof(TextView), new FrameworkPropertyMetadata(Brushes.LightGray)); /// /// Gets/sets the Brush used for displaying non-printable characters. /// public Brush NonPrintableCharacterBrush { get { return (Brush)GetValue(NonPrintableCharacterBrushProperty); } set { SetValue(NonPrintableCharacterBrushProperty, value); } } /// /// LinkTextForegroundBrush dependency property. /// public static readonly DependencyProperty LinkTextForegroundBrushProperty = DependencyProperty.Register("LinkTextForegroundBrush", typeof(Brush), typeof(TextView), new FrameworkPropertyMetadata(Brushes.Blue)); /// /// Gets/sets the Brush used for displaying link texts. /// public Brush LinkTextForegroundBrush { get { return (Brush)GetValue(LinkTextForegroundBrushProperty); } set { SetValue(LinkTextForegroundBrushProperty, value); } } /// /// LinkTextBackgroundBrush dependency property. /// public static readonly DependencyProperty LinkTextBackgroundBrushProperty = DependencyProperty.Register("LinkTextBackgroundBrush", typeof(Brush), typeof(TextView), new FrameworkPropertyMetadata(Brushes.Transparent)); /// /// Gets/sets the Brush used for the background of link texts. /// public Brush LinkTextBackgroundBrush { get { return (Brush)GetValue(LinkTextBackgroundBrushProperty); } set { SetValue(LinkTextBackgroundBrushProperty, value); } } #endregion #region Redraw methods / VisualLine invalidation /// /// Causes the text editor to regenerate all visual lines. /// public void Redraw() { Redraw(DispatcherPriority.Normal); } /// /// Causes the text editor to regenerate all visual lines. /// public void Redraw(DispatcherPriority redrawPriority) { VerifyAccess(); ClearVisualLines(); InvalidateMeasure(redrawPriority); } /// /// Causes the text editor to regenerate the specified visual line. /// public void Redraw(VisualLine visualLine, DispatcherPriority redrawPriority = DispatcherPriority.Normal) { VerifyAccess(); if (allVisualLines.Remove(visualLine)) { DisposeVisualLine(visualLine); InvalidateMeasure(redrawPriority); } } /// /// Causes the text editor to redraw all lines overlapping with the specified segment. /// public void Redraw(int offset, int length, DispatcherPriority redrawPriority = DispatcherPriority.Normal) { VerifyAccess(); bool changedSomethingBeforeOrInLine = false; for (int i = 0; i < allVisualLines.Count; i++) { VisualLine visualLine = allVisualLines[i]; int lineStart = visualLine.FirstDocumentLine.Offset; int lineEnd = visualLine.LastDocumentLine.Offset + visualLine.LastDocumentLine.TotalLength; if (offset <= lineEnd) { changedSomethingBeforeOrInLine = true; if (offset + length >= lineStart) { allVisualLines.RemoveAt(i--); DisposeVisualLine(visualLine); } } } if (changedSomethingBeforeOrInLine) { // Repaint not only when something in visible area was changed, but also when anything in front of it // was changed. We might have to redraw the line number margin. Or the highlighting changed. // However, we'll try to reuse the existing VisualLines. InvalidateMeasure(redrawPriority); } } /// /// Causes a known layer to redraw. /// This method does not invalidate visual lines; /// use the method to do that. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "knownLayer", Justification="This method is meant to invalidate only a specific layer - I just haven't figured out how to do that, yet.")] public void InvalidateLayer(KnownLayer knownLayer) { InvalidateMeasure(DispatcherPriority.Normal); } /// /// Causes a known layer to redraw. /// This method does not invalidate visual lines; /// use the method to do that. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "knownLayer", Justification="This method is meant to invalidate only a specific layer - I just haven't figured out how to do that, yet.")] public void InvalidateLayer(KnownLayer knownLayer, DispatcherPriority priority) { InvalidateMeasure(priority); } /// /// Causes the text editor to redraw all lines overlapping with the specified segment. /// Does nothing if segment is null. /// public void Redraw(ISegment segment, DispatcherPriority redrawPriority = DispatcherPriority.Normal) { if (segment != null) { Redraw(segment.Offset, segment.Length, redrawPriority); } } /// /// Invalidates all visual lines. /// The caller of ClearVisualLines() must also call InvalidateMeasure() to ensure /// that the visual lines will be recreated. /// void ClearVisualLines() { visibleVisualLines = null; if (allVisualLines.Count != 0) { foreach (VisualLine visualLine in allVisualLines) { DisposeVisualLine(visualLine); } allVisualLines.Clear(); } } void DisposeVisualLine(VisualLine visualLine) { if (newVisualLines != null && newVisualLines.Contains(visualLine)) { throw new ArgumentException("Cannot dispose visual line because it is in construction!"); } visibleVisualLines = null; visualLine.Dispose(); RemoveInlineObjects(visualLine); } #endregion #region InvalidateMeasure(DispatcherPriority) DispatcherOperation invalidateMeasureOperation; void InvalidateMeasure(DispatcherPriority priority) { if (priority >= DispatcherPriority.Render) { if (invalidateMeasureOperation != null) { invalidateMeasureOperation.Abort(); invalidateMeasureOperation = null; } base.InvalidateMeasure(); } else { if (invalidateMeasureOperation != null) { invalidateMeasureOperation.Priority = priority; } else { invalidateMeasureOperation = Dispatcher.BeginInvoke( priority, new Action( delegate { invalidateMeasureOperation = null; base.InvalidateMeasure(); } ) ); } } } #endregion #region Get(OrConstruct)VisualLine /// /// Gets the visual line that contains the document line with the specified number. /// Returns null if the document line is outside the visible range. /// public VisualLine GetVisualLine(int documentLineNumber) { // TODO: EnsureVisualLines() ? foreach (VisualLine visualLine in allVisualLines) { Debug.Assert(visualLine.IsDisposed == false); int start = visualLine.FirstDocumentLine.LineNumber; int end = visualLine.LastDocumentLine.LineNumber; if (documentLineNumber >= start && documentLineNumber <= end) return visualLine; } return null; } /// /// Gets the visual line that contains the document line with the specified number. /// If that line is outside the visible range, a new VisualLine for that document line is constructed. /// public VisualLine GetOrConstructVisualLine(DocumentLine documentLine) { if (documentLine == null) throw new ArgumentNullException("documentLine"); if (!this.Document.Lines.Contains(documentLine)) throw new InvalidOperationException("Line belongs to wrong document"); VerifyAccess(); VisualLine l = GetVisualLine(documentLine.LineNumber); if (l == null) { TextRunProperties globalTextRunProperties = CreateGlobalTextRunProperties(); VisualLineTextParagraphProperties paragraphProperties = CreateParagraphProperties(globalTextRunProperties); while (heightTree.GetIsCollapsed(documentLine.LineNumber)) { documentLine = documentLine.PreviousLine; } l = BuildVisualLine(documentLine, globalTextRunProperties, paragraphProperties, elementGenerators.ToArray(), lineTransformers.ToArray(), lastAvailableSize); allVisualLines.Add(l); // update all visual top values (building the line might have changed visual top of other lines due to word wrapping) foreach (var line in allVisualLines) { line.VisualTop = heightTree.GetVisualPosition(line.FirstDocumentLine); } } return l; } #endregion #region Visual Lines (fields and properties) List allVisualLines = new List(); ReadOnlyCollection visibleVisualLines; double clippedPixelsOnTop; List newVisualLines; /// /// Gets the currently visible visual lines. /// /// /// Gets thrown if there are invalid visual lines when this property is accessed. /// You can use the property to check for this case, /// or use the method to force creating the visual lines /// when they are invalid. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations")] public ReadOnlyCollection VisualLines { get { if (visibleVisualLines == null) throw new VisualLinesInvalidException(); return visibleVisualLines; } } /// /// Gets whether the visual lines are valid. /// Will return false after a call to Redraw(). /// Accessing the visual lines property will cause a /// if this property is false. /// public bool VisualLinesValid { get { return visibleVisualLines != null; } } /// /// Occurs when the TextView is about to be measured and will regenerate its visual lines. /// This event may be used to mark visual lines as invalid that would otherwise be reused. /// public event EventHandler VisualLineConstructionStarting; /// /// Occurs when the TextView was measured and changed its visual lines. /// public event EventHandler VisualLinesChanged; /// /// If the visual lines are invalid, creates new visual lines for the visible part /// of the document. /// If all visual lines are valid, this method does nothing. /// /// The visual line build process is already running. /// It is not allowed to call this method during the construction of a visual line. public void EnsureVisualLines() { Dispatcher.VerifyAccess(); if (inMeasure) throw new InvalidOperationException("The visual line build process is already running! Cannot EnsureVisualLines() during Measure!"); if (!VisualLinesValid) { // increase priority for re-measure InvalidateMeasure(DispatcherPriority.Normal); // force immediate re-measure UpdateLayout(); } // Sometimes we still have invalid lines after UpdateLayout - work around the problem // by calling MeasureOverride directly. if (!VisualLinesValid) { Debug.WriteLine("UpdateLayout() failed in EnsureVisualLines"); MeasureOverride(lastAvailableSize); } if (!VisualLinesValid) throw new VisualLinesInvalidException("Internal error: visual lines invalid after EnsureVisualLines call"); } #endregion #region Measure /// /// Additonal amount that allows horizontal scrolling past the end of the longest line. /// This is necessary to ensure the caret always is visible, even when it is at the end of the longest line. /// const double AdditionalHorizontalScrollAmount = 3; Size lastAvailableSize; bool inMeasure; /// protected override Size MeasureOverride(Size availableSize) { // We don't support infinite available width, so we'll limit it to 32000 pixels. if (availableSize.Width > 32000) availableSize.Width = 32000; if (!canHorizontallyScroll && !availableSize.Width.IsClose(lastAvailableSize.Width)) ClearVisualLines(); lastAvailableSize = availableSize; foreach (UIElement layer in layers) { layer.Measure(availableSize); } MeasureInlineObjects(); InvalidateVisual(); // = InvalidateArrange+InvalidateRender double maxWidth; if (document == null) { // no document -> create empty list of lines allVisualLines = new List(); visibleVisualLines = allVisualLines.AsReadOnly(); maxWidth = 0; } else { inMeasure = true; try { maxWidth = CreateAndMeasureVisualLines(availableSize); } finally { inMeasure = false; } } // remove inline objects only at the end, so that inline objects that were re-used are not removed from the editor RemoveInlineObjectsNow(); maxWidth += AdditionalHorizontalScrollAmount; double heightTreeHeight = this.DocumentHeight; TextEditorOptions options = this.Options; if (options.AllowScrollBelowDocument) { if (!double.IsInfinity(scrollViewport.Height)) { heightTreeHeight = Math.Max(heightTreeHeight, Math.Min(heightTreeHeight - 50, scrollOffset.Y) + scrollViewport.Height); } } textLayer.SetVisualLines(visibleVisualLines); SetScrollData(availableSize, new Size(maxWidth, heightTreeHeight), scrollOffset); if (VisualLinesChanged != null) VisualLinesChanged(this, EventArgs.Empty); return new Size(Math.Min(availableSize.Width, maxWidth), Math.Min(availableSize.Height, heightTreeHeight)); } /// /// Build all VisualLines in the visible range. /// /// Width the longest line double CreateAndMeasureVisualLines(Size availableSize) { TextRunProperties globalTextRunProperties = CreateGlobalTextRunProperties(); VisualLineTextParagraphProperties paragraphProperties = CreateParagraphProperties(globalTextRunProperties); Debug.WriteLine("Measure availableSize=" + availableSize + ", scrollOffset=" + scrollOffset); var firstLineInView = heightTree.GetLineByVisualPosition(scrollOffset.Y); // number of pixels clipped from the first visual line(s) clippedPixelsOnTop = scrollOffset.Y - heightTree.GetVisualPosition(firstLineInView); // clippedPixelsOnTop should be >= 0, except for floating point inaccurracy. Debug.Assert(clippedPixelsOnTop >= -ExtensionMethods.Epsilon); newVisualLines = new List(); if (VisualLineConstructionStarting != null) VisualLineConstructionStarting(this, new VisualLineConstructionStartEventArgs(firstLineInView)); var elementGeneratorsArray = elementGenerators.ToArray(); var lineTransformersArray = lineTransformers.ToArray(); var nextLine = firstLineInView; double maxWidth = 0; double yPos = -clippedPixelsOnTop; while (yPos < availableSize.Height && nextLine != null) { VisualLine visualLine = GetVisualLine(nextLine.LineNumber); if (visualLine == null) { visualLine = BuildVisualLine(nextLine, globalTextRunProperties, paragraphProperties, elementGeneratorsArray, lineTransformersArray, availableSize); } visualLine.VisualTop = scrollOffset.Y + yPos; nextLine = visualLine.LastDocumentLine.NextLine; yPos += visualLine.Height; foreach (TextLine textLine in visualLine.TextLines) { if (textLine.WidthIncludingTrailingWhitespace > maxWidth) maxWidth = textLine.WidthIncludingTrailingWhitespace; } newVisualLines.Add(visualLine); } foreach (VisualLine line in allVisualLines) { Debug.Assert(line.IsDisposed == false); if (!newVisualLines.Contains(line)) DisposeVisualLine(line); } allVisualLines = newVisualLines; // visibleVisualLines = readonly copy of visual lines visibleVisualLines = new ReadOnlyCollection(newVisualLines.ToArray()); newVisualLines = null; if (allVisualLines.Any(line => line.IsDisposed)) { throw new InvalidOperationException("A visual line was disposed even though it is still in use.\n" + "This can happen when Redraw() is called during measure for lines " + "that are already constructed."); } return maxWidth; } #endregion #region BuildVisualLine TextFormatter formatter; internal TextViewCachedElements cachedElements; TextRunProperties CreateGlobalTextRunProperties() { var p = new GlobalTextRunProperties(); p.typeface = this.CreateTypeface(); p.fontRenderingEmSize = FontSize; p.foregroundBrush = (Brush)GetValue(Control.ForegroundProperty); ExtensionMethods.CheckIsFrozen(p.foregroundBrush); p.cultureInfo = CultureInfo.CurrentCulture; return p; } VisualLineTextParagraphProperties CreateParagraphProperties(TextRunProperties defaultTextRunProperties) { return new VisualLineTextParagraphProperties { defaultTextRunProperties = defaultTextRunProperties, textWrapping = canHorizontallyScroll ? TextWrapping.NoWrap : TextWrapping.Wrap, tabSize = Options.IndentationSize * WideSpaceWidth }; } VisualLine BuildVisualLine(DocumentLine documentLine, TextRunProperties globalTextRunProperties, VisualLineTextParagraphProperties paragraphProperties, VisualLineElementGenerator[] elementGeneratorsArray, IVisualLineTransformer[] lineTransformersArray, Size availableSize) { if (heightTree.GetIsCollapsed(documentLine.LineNumber)) throw new InvalidOperationException("Trying to build visual line from collapsed line"); //Debug.WriteLine("Building line " + documentLine.LineNumber); VisualLine visualLine = new VisualLine(this, documentLine); VisualLineTextSource textSource = new VisualLineTextSource(visualLine) { Document = document, GlobalTextRunProperties = globalTextRunProperties, TextView = this }; visualLine.ConstructVisualElements(textSource, elementGeneratorsArray); if (visualLine.FirstDocumentLine != visualLine.LastDocumentLine) { // Check whether the lines are collapsed correctly: double firstLinePos = heightTree.GetVisualPosition(visualLine.FirstDocumentLine.NextLine); double lastLinePos = heightTree.GetVisualPosition(visualLine.LastDocumentLine.NextLine ?? visualLine.LastDocumentLine); if (!firstLinePos.IsClose(lastLinePos)) { for (int i = visualLine.FirstDocumentLine.LineNumber + 1; i <= visualLine.LastDocumentLine.LineNumber; i++) { if (!heightTree.GetIsCollapsed(i)) throw new InvalidOperationException("Line " + i + " was skipped by a VisualLineElementGenerator, but it is not collapsed."); } throw new InvalidOperationException("All lines collapsed but visual pos different - height tree inconsistency?"); } } visualLine.RunTransformers(textSource, lineTransformersArray); // now construct textLines: int textOffset = 0; TextLineBreak lastLineBreak = null; var textLines = new List(); paragraphProperties.indent = 0; paragraphProperties.firstLineInParagraph = true; while (textOffset <= visualLine.VisualLengthWithEndOfLineMarker) { TextLine textLine = formatter.FormatLine( textSource, textOffset, availableSize.Width, paragraphProperties, lastLineBreak ); textLines.Add(textLine); textOffset += textLine.Length; // exit loop so that we don't do the indentation calculation if there's only a single line if (textOffset >= visualLine.VisualLengthWithEndOfLineMarker) break; if (paragraphProperties.firstLineInParagraph) { paragraphProperties.firstLineInParagraph = false; TextEditorOptions options = this.Options; double indentation = 0; if (options.InheritWordWrapIndentation) { // determine indentation for next line: int indentVisualColumn = GetIndentationVisualColumn(visualLine); if (indentVisualColumn > 0 && indentVisualColumn < textOffset) { indentation = textLine.GetDistanceFromCharacterHit(new CharacterHit(indentVisualColumn, 0)); } } indentation += options.WordWrapIndentation; // apply the calculated indentation unless it's more than half of the text editor size: if (indentation > 0 && indentation * 2 < availableSize.Width) paragraphProperties.indent = indentation; } lastLineBreak = textLine.GetTextLineBreak(); } visualLine.SetTextLines(textLines); heightTree.SetHeight(visualLine.FirstDocumentLine, visualLine.Height); return visualLine; } static int GetIndentationVisualColumn(VisualLine visualLine) { if (visualLine.Elements.Count == 0) return 0; int column = 0; int elementIndex = 0; VisualLineElement element = visualLine.Elements[elementIndex]; while (element.IsWhitespace(column)) { column++; if (column == element.VisualColumn + element.VisualLength) { elementIndex++; if (elementIndex == visualLine.Elements.Count) break; element = visualLine.Elements[elementIndex]; } } return column; } #endregion #region Arrange /// /// Arrange implementation. /// protected override Size ArrangeOverride(Size finalSize) { EnsureVisualLines(); foreach (UIElement layer in layers) { layer.Arrange(new Rect(new Point(0, 0), finalSize)); } if (document == null || allVisualLines.Count == 0) return finalSize; // validate scroll position Vector newScrollOffset = scrollOffset; if (scrollOffset.X + finalSize.Width > scrollExtent.Width) { newScrollOffset.X = Math.Max(0, scrollExtent.Width - finalSize.Width); } if (scrollOffset.Y + finalSize.Height > scrollExtent.Height) { newScrollOffset.Y = Math.Max(0, scrollExtent.Height - finalSize.Height); } if (SetScrollData(scrollViewport, scrollExtent, newScrollOffset)) InvalidateMeasure(DispatcherPriority.Normal); //Debug.WriteLine("Arrange finalSize=" + finalSize + ", scrollOffset=" + scrollOffset); // double maxWidth = 0; if (visibleVisualLines != null) { Point pos = new Point(-scrollOffset.X, -clippedPixelsOnTop); foreach (VisualLine visualLine in visibleVisualLines) { int offset = 0; foreach (TextLine textLine in visualLine.TextLines) { foreach (var span in textLine.GetTextRunSpans()) { InlineObjectRun inline = span.Value as InlineObjectRun; if (inline != null && inline.VisualLine != null) { Debug.Assert(inlineObjects.Contains(inline)); double distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(offset, 0)); inline.Element.Arrange(new Rect(new Point(pos.X + distance, pos.Y), inline.Element.DesiredSize)); } offset += span.Length; } pos.Y += textLine.Height; } } } InvalidateCursorIfMouseWithinTextView(); return finalSize; } #endregion #region Render readonly ObserveAddRemoveCollection backgroundRenderers; /// /// Gets the list of background renderers. /// public IList BackgroundRenderers { get { return backgroundRenderers; } } void BackgroundRenderer_Added(IBackgroundRenderer renderer) { ConnectToTextView(renderer); InvalidateLayer(renderer.Layer); } void BackgroundRenderer_Removed(IBackgroundRenderer renderer) { DisconnectFromTextView(renderer); InvalidateLayer(renderer.Layer); } /// protected override void OnRender(DrawingContext drawingContext) { RenderBackground(drawingContext, KnownLayer.Background); foreach (var line in visibleVisualLines) { Brush currentBrush = null; int startVC = 0; int length = 0; foreach (var element in line.Elements) { if (currentBrush == null || !currentBrush.Equals(element.BackgroundBrush)) { if (currentBrush != null) { BackgroundGeometryBuilder builder = new BackgroundGeometryBuilder(); builder.AlignToWholePixels = true; builder.CornerRadius = 3; foreach (var rect in BackgroundGeometryBuilder.GetRectsFromVisualSegment(this, line, startVC, startVC + length)) builder.AddRectangle(this, rect); Geometry geometry = builder.CreateGeometry(); if (geometry != null) { drawingContext.DrawGeometry(currentBrush, null, geometry); } } startVC = element.VisualColumn; length = element.DocumentLength; currentBrush = element.BackgroundBrush; } else { length += element.VisualLength; } } if (currentBrush != null) { BackgroundGeometryBuilder builder = new BackgroundGeometryBuilder(); builder.AlignToWholePixels = true; builder.CornerRadius = 3; foreach (var rect in BackgroundGeometryBuilder.GetRectsFromVisualSegment(this, line, startVC, startVC + length)) builder.AddRectangle(this, rect); Geometry geometry = builder.CreateGeometry(); if (geometry != null) { drawingContext.DrawGeometry(currentBrush, null, geometry); } } } } internal void RenderBackground(DrawingContext drawingContext, KnownLayer layer) { foreach (IBackgroundRenderer bg in backgroundRenderers) { if (bg.Layer == layer) { bg.Draw(this, drawingContext); } } } internal void ArrangeTextLayer(IList visuals) { Point pos = new Point(-scrollOffset.X, -clippedPixelsOnTop); foreach (VisualLineDrawingVisual visual in visuals) { TranslateTransform t = visual.Transform as TranslateTransform; if (t == null || t.X != pos.X || t.Y != pos.Y) { visual.Transform = new TranslateTransform(pos.X, pos.Y); visual.Transform.Freeze(); } pos.Y += visual.Height; } } #endregion #region IScrollInfo implementation /// /// Size of the document, in pixels. /// Size scrollExtent; /// /// Offset of the scroll position. /// Vector scrollOffset; /// /// Size of the viewport. /// Size scrollViewport; void ClearScrollData() { SetScrollData(new Size(), new Size(), new Vector()); } bool SetScrollData(Size viewport, Size extent, Vector offset) { if (!(viewport.IsClose(this.scrollViewport) && extent.IsClose(this.scrollExtent) && offset.IsClose(this.scrollOffset))) { this.scrollViewport = viewport; this.scrollExtent = extent; SetScrollOffset(offset); this.OnScrollChange(); return true; } return false; } void OnScrollChange() { ScrollViewer scrollOwner = ((IScrollInfo)this).ScrollOwner; if (scrollOwner != null) { scrollOwner.InvalidateScrollInfo(); } } bool canVerticallyScroll; bool IScrollInfo.CanVerticallyScroll { get { return canVerticallyScroll; } set { if (canVerticallyScroll != value) { canVerticallyScroll = value; InvalidateMeasure(DispatcherPriority.Normal); } } } bool canHorizontallyScroll; bool IScrollInfo.CanHorizontallyScroll { get { return canHorizontallyScroll; } set { if (canHorizontallyScroll != value) { canHorizontallyScroll = value; ClearVisualLines(); InvalidateMeasure(DispatcherPriority.Normal); } } } double IScrollInfo.ExtentWidth { get { return scrollExtent.Width; } } double IScrollInfo.ExtentHeight { get { return scrollExtent.Height; } } double IScrollInfo.ViewportWidth { get { return scrollViewport.Width; } } double IScrollInfo.ViewportHeight { get { return scrollViewport.Height; } } /// /// Gets the horizontal scroll offset. /// public double HorizontalOffset { get { return scrollOffset.X; } } /// /// Gets the vertical scroll offset. /// public double VerticalOffset { get { return scrollOffset.Y; } } /// /// Gets the scroll offset; /// public Vector ScrollOffset { get { return scrollOffset; } } /// /// Occurs when the scroll offset has changed. /// public event EventHandler ScrollOffsetChanged; void SetScrollOffset(Vector vector) { if (!canHorizontallyScroll) vector.X = 0; if (!canVerticallyScroll) vector.Y = 0; if (!scrollOffset.IsClose(vector)) { scrollOffset = vector; if (ScrollOffsetChanged != null) ScrollOffsetChanged(this, EventArgs.Empty); } } ScrollViewer IScrollInfo.ScrollOwner { get; set; } void IScrollInfo.LineUp() { ((IScrollInfo)this).SetVerticalOffset(scrollOffset.Y - DefaultLineHeight); } void IScrollInfo.LineDown() { ((IScrollInfo)this).SetVerticalOffset(scrollOffset.Y + DefaultLineHeight); } void IScrollInfo.LineLeft() { ((IScrollInfo)this).SetHorizontalOffset(scrollOffset.X - WideSpaceWidth); } void IScrollInfo.LineRight() { ((IScrollInfo)this).SetHorizontalOffset(scrollOffset.X + WideSpaceWidth); } void IScrollInfo.PageUp() { ((IScrollInfo)this).SetVerticalOffset(scrollOffset.Y - scrollViewport.Height); } void IScrollInfo.PageDown() { ((IScrollInfo)this).SetVerticalOffset(scrollOffset.Y + scrollViewport.Height); } void IScrollInfo.PageLeft() { ((IScrollInfo)this).SetHorizontalOffset(scrollOffset.X - scrollViewport.Width); } void IScrollInfo.PageRight() { ((IScrollInfo)this).SetHorizontalOffset(scrollOffset.X + scrollViewport.Width); } void IScrollInfo.MouseWheelUp() { ((IScrollInfo)this).SetVerticalOffset( scrollOffset.Y - (SystemParameters.WheelScrollLines * DefaultLineHeight)); OnScrollChange(); } void IScrollInfo.MouseWheelDown() { ((IScrollInfo)this).SetVerticalOffset( scrollOffset.Y + (SystemParameters.WheelScrollLines * DefaultLineHeight)); OnScrollChange(); } void IScrollInfo.MouseWheelLeft() { ((IScrollInfo)this).SetHorizontalOffset( scrollOffset.X - (SystemParameters.WheelScrollLines * WideSpaceWidth)); OnScrollChange(); } void IScrollInfo.MouseWheelRight() { ((IScrollInfo)this).SetHorizontalOffset( scrollOffset.X + (SystemParameters.WheelScrollLines * WideSpaceWidth)); OnScrollChange(); } bool defaultTextMetricsValid; 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 defaultBaseline; // Baseline of a line containing 'x'. Used for TextTop/TextBottom calculation. /// /// Gets the width of a 'wide space' (the space width used for calculating the tab size). /// /// /// This is the width of an 'x' in the current font. /// We do not measure the width of an actual space as that would lead to tiny tabs in /// some proportional fonts. /// For monospaced fonts, this property will return the expected value, as 'x' and ' ' have the same width. /// public double WideSpaceWidth { get { CalculateDefaultTextMetrics(); return wideSpaceWidth; } } /// /// Gets the default line height. This is the height of an empty line or a line containing regular text. /// Lines that include formatted text or custom UI elements may have a different line height. /// public double DefaultLineHeight { get { CalculateDefaultTextMetrics(); return defaultLineHeight; } } /// /// Gets the default baseline position. This is the difference between /// and for a line containing regular text. /// Lines that include formatted text or custom UI elements may have a different baseline. /// public double DefaultBaseline { get { CalculateDefaultTextMetrics(); return defaultBaseline; } } void InvalidateDefaultTextMetrics() { defaultTextMetricsValid = false; if (heightTree != null) { // calculate immediately so that height tree gets updated CalculateDefaultTextMetrics(); } } void CalculateDefaultTextMetrics() { if (defaultTextMetricsValid) return; defaultTextMetricsValid = true; if (formatter != null) { var textRunProperties = CreateGlobalTextRunProperties(); using (var line = formatter.FormatLine( new SimpleTextSource("x", textRunProperties), 0, 32000, new VisualLineTextParagraphProperties { defaultTextRunProperties = textRunProperties }, null)) { wideSpaceWidth = Math.Max(1, line.WidthIncludingTrailingWhitespace); defaultBaseline = Math.Max(1, line.Baseline); defaultLineHeight = Math.Max(1, line.Height); } } else { wideSpaceWidth = FontSize / 2; defaultBaseline = FontSize; defaultLineHeight = FontSize + 3; } // Update heightTree.DefaultLineHeight, if a document is loaded. if (heightTree != null) heightTree.DefaultLineHeight = defaultLineHeight; } static double ValidateVisualOffset(double offset) { if (double.IsNaN(offset)) throw new ArgumentException("offset must not be NaN"); if (offset < 0) return 0; else return offset; } void IScrollInfo.SetHorizontalOffset(double offset) { offset = ValidateVisualOffset(offset); if (!scrollOffset.X.IsClose(offset)) { SetScrollOffset(new Vector(offset, scrollOffset.Y)); InvalidateVisual(); textLayer.InvalidateVisual(); } } void IScrollInfo.SetVerticalOffset(double offset) { offset = ValidateVisualOffset(offset); if (!scrollOffset.Y.IsClose(offset)) { SetScrollOffset(new Vector(scrollOffset.X, offset)); InvalidateMeasure(DispatcherPriority.Normal); } } Rect IScrollInfo.MakeVisible(Visual visual, Rect rectangle) { if (rectangle.IsEmpty || visual == null || visual == this || !this.IsAncestorOf(visual)) { return Rect.Empty; } // Convert rectangle into our coordinate space. GeneralTransform childTransform = visual.TransformToAncestor(this); rectangle = childTransform.TransformBounds(rectangle); MakeVisible(Rect.Offset(rectangle, scrollOffset)); return rectangle; } /// /// Scrolls the text view so that the specified rectangle gets visible. /// public void MakeVisible(Rect rectangle) { Rect visibleRectangle = new Rect(scrollOffset.X, scrollOffset.Y, scrollViewport.Width, scrollViewport.Height); Vector newScrollOffset = scrollOffset; if (rectangle.Left < visibleRectangle.Left) { if (rectangle.Right > visibleRectangle.Right) { newScrollOffset.X = rectangle.Left + rectangle.Width / 2; } else { newScrollOffset.X = rectangle.Left; } } else if (rectangle.Right > visibleRectangle.Right) { newScrollOffset.X = rectangle.Right - scrollViewport.Width; } if (rectangle.Top < visibleRectangle.Top) { if (rectangle.Bottom > visibleRectangle.Bottom) { newScrollOffset.Y = rectangle.Top + rectangle.Height / 2; } else { newScrollOffset.Y = rectangle.Top; } } else if (rectangle.Bottom > visibleRectangle.Bottom) { newScrollOffset.Y = rectangle.Bottom - scrollViewport.Height; } newScrollOffset.X = ValidateVisualOffset(newScrollOffset.X); newScrollOffset.Y = ValidateVisualOffset(newScrollOffset.Y); if (!scrollOffset.IsClose(newScrollOffset)) { SetScrollOffset(newScrollOffset); this.OnScrollChange(); InvalidateMeasure(DispatcherPriority.Normal); } } #endregion #region Visual element mouse handling /// protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters) { // accept clicks even where the text area draws no background return new PointHitTestResult(this, hitTestParameters.HitPoint); } [ThreadStatic] static bool invalidCursor; /// /// Updates the mouse cursor by calling , but with background priority. /// public static void InvalidateCursor() { if (!invalidCursor) { invalidCursor = true; Dispatcher.CurrentDispatcher.BeginInvoke( DispatcherPriority.Background, // fixes issue #288 new Action( delegate { invalidCursor = false; Mouse.UpdateCursor(); })); } } internal void InvalidateCursorIfMouseWithinTextView() { // Don't unnecessarily call Mouse.UpdateCursor() if the mouse is outside the text view. // Unnecessary updates may cause the mouse pointer to flicker // (e.g. if it is over a window border, it blinks between Resize and Normal) if (this.IsMouseOver) InvalidateCursor(); } /// protected override void OnQueryCursor(QueryCursorEventArgs e) { VisualLineElement element = GetVisualLineElementFromPosition(e.GetPosition(this) + scrollOffset); if (element != null) { element.OnQueryCursor(e); } } /// protected override void OnMouseDown(MouseButtonEventArgs e) { base.OnMouseDown(e); if (!e.Handled) { EnsureVisualLines(); VisualLineElement element = GetVisualLineElementFromPosition(e.GetPosition(this) + scrollOffset); if (element != null) { element.OnMouseDown(e); } } } /// protected override void OnMouseUp(MouseButtonEventArgs e) { base.OnMouseUp(e); if (!e.Handled) { EnsureVisualLines(); VisualLineElement element = GetVisualLineElementFromPosition(e.GetPosition(this) + scrollOffset); if (element != null) { element.OnMouseUp(e); } } } #endregion #region Getting elements from Visual Position /// /// Gets the visual line at the specified document position (relative to start of document). /// Returns null if there is no visual line for the position (e.g. the position is outside the visible /// text area). /// public VisualLine GetVisualLineFromVisualTop(double visualTop) { // TODO: change this method to also work outside the visible range - // required to make GetPosition work as expected! EnsureVisualLines(); foreach (VisualLine vl in this.VisualLines) { if (visualTop < vl.VisualTop) continue; if (visualTop < vl.VisualTop + vl.Height) return vl; } return null; } /// /// Gets the visual top position (relative to start of document) from a document line number. /// public double GetVisualTopByDocumentLine(int line) { VerifyAccess(); if (heightTree == null) throw ThrowUtil.NoDocumentAssigned(); return heightTree.GetVisualPosition(heightTree.GetLineByNumber(line)); } VisualLineElement GetVisualLineElementFromPosition(Point visualPosition) { VisualLine vl = GetVisualLineFromVisualTop(visualPosition.Y); if (vl != null) { int column = vl.GetVisualColumnFloor(visualPosition); // Debug.WriteLine(vl.FirstDocumentLine.LineNumber + " vc " + column); foreach (VisualLineElement element in vl.Elements) { if (element.VisualColumn + element.VisualLength <= column) continue; return element; } } return null; } #endregion #region Visual Position <-> TextViewPosition /// /// Gets the visual position from a text view position. /// /// The text view position. /// The mode how to retrieve the Y position. /// The position in WPF device-independent pixels relative /// to the top left corner of the document. public Point GetVisualPosition(TextViewPosition position, VisualYPosition yPositionMode) { VerifyAccess(); if (this.Document == null) throw ThrowUtil.NoDocumentAssigned(); DocumentLine documentLine = this.Document.GetLineByNumber(position.Line); VisualLine visualLine = GetOrConstructVisualLine(documentLine); int visualColumn = position.VisualColumn; if (visualColumn < 0) { int offset = documentLine.Offset + position.Column - 1; visualColumn = visualLine.GetVisualColumn(offset - visualLine.FirstDocumentLine.Offset); } return visualLine.GetVisualPosition(visualColumn, position.IsAtEndOfLine, yPositionMode); } /// /// Gets the text view position from the specified visual position. /// If the position is within a character, it is rounded to the next character boundary. /// /// The position in WPF device-independent pixels relative /// to the top left corner of the document. /// The logical position, or null if the position is outside the document. public TextViewPosition? GetPosition(Point visualPosition) { VerifyAccess(); if (this.Document == null) throw ThrowUtil.NoDocumentAssigned(); VisualLine line = GetVisualLineFromVisualTop(visualPosition.Y); if (line == null) return null; return line.GetTextViewPosition(visualPosition, Options.EnableVirtualSpace); } /// /// Gets the text view position from the specified visual position. /// If the position is inside a character, the position in front of the character is returned. /// /// The position in WPF device-independent pixels relative /// to the top left corner of the document. /// The logical position, or null if the position is outside the document. public TextViewPosition? GetPositionFloor(Point visualPosition) { VerifyAccess(); if (this.Document == null) throw ThrowUtil.NoDocumentAssigned(); VisualLine line = GetVisualLineFromVisualTop(visualPosition.Y); if (line == null) return null; return line.GetTextViewPositionFloor(visualPosition, Options.EnableVirtualSpace); } #endregion #region Service Provider readonly ServiceContainer services = new ServiceContainer(); /// /// Gets a service container used to associate services with the text view. /// /// /// This container does not provide document services - /// use TextView.GetService() instead of TextView.Services.GetService() to ensure /// that document services can be found as well. /// public ServiceContainer Services { get { return services; } } /// /// Retrieves a service from the text view. /// If the service is not found in the container, /// this method will also look for it in the current document's service provider. /// public virtual object GetService(Type serviceType) { object instance = services.GetService(serviceType); if (instance == null && document != null) { instance = document.ServiceProvider.GetService(serviceType); } return instance; } void ConnectToTextView(object obj) { ITextViewConnect c = obj as ITextViewConnect; if (c != null) c.AddToTextView(this); } void DisconnectFromTextView(object obj) { ITextViewConnect c = obj as ITextViewConnect; if (c != null) c.RemoveFromTextView(this); } #endregion #region MouseHover /// /// The PreviewMouseHover event. /// public static readonly RoutedEvent PreviewMouseHoverEvent = EventManager.RegisterRoutedEvent("PreviewMouseHover", RoutingStrategy.Tunnel, typeof(MouseEventHandler), typeof(TextView)); /// /// The MouseHover event. /// public static readonly RoutedEvent MouseHoverEvent = EventManager.RegisterRoutedEvent("MouseHover", RoutingStrategy.Bubble, typeof(MouseEventHandler), typeof(TextView)); /// /// The PreviewMouseHoverStopped event. /// public static readonly RoutedEvent PreviewMouseHoverStoppedEvent = EventManager.RegisterRoutedEvent("PreviewMouseHoverStopped", RoutingStrategy.Tunnel, typeof(MouseEventHandler), typeof(TextView)); /// /// The MouseHoverStopped event. /// public static readonly RoutedEvent MouseHoverStoppedEvent = EventManager.RegisterRoutedEvent("MouseHoverStopped", RoutingStrategy.Bubble, typeof(MouseEventHandler), typeof(TextView)); /// /// Occurs when the mouse has hovered over a fixed location for some time. /// public event MouseEventHandler PreviewMouseHover { add { AddHandler(PreviewMouseHoverEvent, value); } remove { RemoveHandler(PreviewMouseHoverEvent, value); } } /// /// Occurs when the mouse has hovered over a fixed location for some time. /// public event MouseEventHandler MouseHover { add { AddHandler(MouseHoverEvent, value); } remove { RemoveHandler(MouseHoverEvent, value); } } /// /// Occurs when the mouse had previously hovered but now started moving again. /// public event MouseEventHandler PreviewMouseHoverStopped { add { AddHandler(PreviewMouseHoverStoppedEvent, value); } remove { RemoveHandler(PreviewMouseHoverStoppedEvent, value); } } /// /// Occurs when the mouse had previously hovered but now started moving again. /// public event MouseEventHandler MouseHoverStopped { add { AddHandler(MouseHoverStoppedEvent, value); } remove { RemoveHandler(MouseHoverStoppedEvent, value); } } MouseHoverLogic hoverLogic; void RaiseHoverEventPair(MouseEventArgs e, RoutedEvent tunnelingEvent, RoutedEvent bubblingEvent) { var mouseDevice = e.MouseDevice; var stylusDevice = e.StylusDevice; int inputTime = Environment.TickCount; var args1 = new MouseEventArgs(mouseDevice, inputTime, stylusDevice) { RoutedEvent = tunnelingEvent, Source = this }; RaiseEvent(args1); var args2 = new MouseEventArgs(mouseDevice, inputTime, stylusDevice) { RoutedEvent = bubblingEvent, Source = this, Handled = args1.Handled }; RaiseEvent(args2); } #endregion /// /// Collapses lines for the purpose of scrolling. s marked as collapsed will be hidden /// and not used to start the generation of a . /// /// /// This method is meant for s that cause s to span /// multiple s. Do not call it without providing a corresponding /// . /// If you want to create collapsible text sections, see . /// /// Note that if you want a VisualLineElement to span from line N to line M, then you need to collapse only the lines /// N+1 to M. Do not collapse line N itself. /// /// When you no longer need the section to be collapsed, call on the /// returned from this method. /// public CollapsedLineSection CollapseLines(DocumentLine start, DocumentLine end) { VerifyAccess(); if (heightTree == null) throw ThrowUtil.NoDocumentAssigned(); return heightTree.CollapseText(start, end); } /// /// Gets the height of the document. /// public double DocumentHeight { get { // return 0 if there is no document = no heightTree return heightTree != null ? heightTree.TotalHeight : 0; } } /// /// Gets the document line at the specified visual position. /// public DocumentLine GetDocumentLineByVisualTop(double visualTop) { VerifyAccess(); if (heightTree == null) throw ThrowUtil.NoDocumentAssigned(); return heightTree.GetLineByVisualPosition(visualTop); } /// protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) { base.OnPropertyChanged(e); if (TextFormatterFactory.PropertyChangeAffectsTextFormatter(e.Property)) { // first, create the new text formatter: RecreateTextFormatter(); // changing text formatter requires recreating the cached elements RecreateCachedElements(); // and we need to re-measure the font metrics: InvalidateDefaultTextMetrics(); } else if (e.Property == Control.ForegroundProperty || e.Property == TextView.NonPrintableCharacterBrushProperty || e.Property == TextView.LinkTextBackgroundBrushProperty || e.Property == TextView.LinkTextForegroundBrushProperty) { // changing brushes requires recreating the cached elements RecreateCachedElements(); Redraw(); } if (e.Property == Control.FontFamilyProperty || e.Property == Control.FontSizeProperty || e.Property == Control.FontStretchProperty || e.Property == Control.FontStyleProperty || e.Property == Control.FontWeightProperty) { // changing font properties requires recreating cached elements RecreateCachedElements(); // and we need to re-measure the font metrics: InvalidateDefaultTextMetrics(); Redraw(); } if (e.Property == ColumnRulerPenProperty) { columnRulerRenderer.SetRuler(this.Options.ColumnRulerPosition, this.ColumnRulerPen); } if (e.Property == CurrentLineBorderProperty) { currentLineHighlighRenderer.BorderPen = this.CurrentLineBorder; } if (e.Property == CurrentLineBackgroundProperty) { currentLineHighlighRenderer.BackgroundBrush = this.CurrentLineBackground; } } /// /// The pen used to draw the column ruler. /// /// public static readonly DependencyProperty ColumnRulerPenProperty = DependencyProperty.Register("ColumnRulerBrush", typeof(Pen), typeof(TextView), new FrameworkPropertyMetadata(CreateFrozenPen(Brushes.LightGray))); static Pen CreateFrozenPen(SolidColorBrush brush) { Pen pen = new Pen(brush, 1); pen.Freeze(); return pen; } /// /// Gets/Sets the pen used to draw the column ruler. /// /// public Pen ColumnRulerPen { get { return (Pen)GetValue(ColumnRulerPenProperty); } set { SetValue(ColumnRulerPenProperty, value); } } /// /// The property. /// public static readonly DependencyProperty CurrentLineBackgroundProperty = DependencyProperty.Register("CurrentLineBackground", typeof(Brush), typeof(TextView)); /// /// Gets/Sets the background brush used by current line highlighter. /// public Brush CurrentLineBackground { get { return (Brush)GetValue(CurrentLineBackgroundProperty); } set { SetValue(CurrentLineBackgroundProperty, value); } } /// /// The property. /// public static readonly DependencyProperty CurrentLineBorderProperty = DependencyProperty.Register("CurrentLineBorder", typeof(Pen), typeof(TextView)); /// /// Gets/Sets the background brush used for the current line. /// public Pen CurrentLineBorder { get { return (Pen)GetValue(CurrentLineBorderProperty); } set { SetValue(CurrentLineBorderProperty, value); } } /// /// Gets/Sets highlighted line number. /// public int HighlightedLine { get { return this.currentLineHighlighRenderer.Line; } set { this.currentLineHighlighRenderer.Line = value; } } } }