diff options
| author | Victoria Plitt <Victoria.Plitt@twine-s.com> | 2019-04-08 13:49:55 +0300 |
|---|---|---|
| committer | Victoria Plitt <Victoria.Plitt@twine-s.com> | 2019-04-08 13:49:55 +0300 |
| commit | fc8a05358a92cc3c77c5f1e30d536807ef0614fd (patch) | |
| tree | c65f696ebd60f3790145721307c255e5a339923f /Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextView.cs | |
| parent | b4a71931ea52636c6b36376aa9d71697ccf73524 (diff) | |
| download | Tango-fc8a05358a92cc3c77c5f1e30d536807ef0614fd.tar.gz Tango-fc8a05358a92cc3c77c5f1e30d536807ef0614fd.zip | |
were added scripting projects
Diffstat (limited to 'Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextView.cs')
| -rw-r--r-- | Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextView.cs | 2009 |
1 files changed, 2009 insertions, 0 deletions
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextView.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextView.cs new file mode 100644 index 000000000..3dabb6b7a --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextView.cs @@ -0,0 +1,2009 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +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 Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Editing; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Rendering +{ + /// <summary> + /// A virtualizing panel producing+showing <see cref="VisualLine"/>s for a <see cref="TextDocument"/>. + /// + /// 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. + /// </summary> + [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; + + /// <summary> + /// Creates a new TextView instance. + /// </summary> + public TextView() + { + services.AddService(typeof(TextView), this); + textLayer = new TextLayer(this); + elementGenerators = new ObserveAddRemoveCollection<VisualLineElementGenerator>(ElementGenerator_Added, ElementGenerator_Removed); + lineTransformers = new ObserveAddRemoveCollection<IVisualLineTransformer>(LineTransformer_Added, LineTransformer_Removed); + backgroundRenderers = new ObserveAddRemoveCollection<IBackgroundRenderer>(BackgroundRenderer_Added, BackgroundRenderer_Removed); + columnRulerRenderer = new ColumnRulerRenderer(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 + /// <summary> + /// Document property. + /// </summary> + public static readonly DependencyProperty DocumentProperty = + DependencyProperty.Register("Document", typeof(TextDocument), typeof(TextView), + new FrameworkPropertyMetadata(OnDocumentChanged)); + + TextDocument document; + HeightTree heightTree; + + /// <summary> + /// Gets/Sets the document displayed by the text editor. + /// </summary> + 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); + } + } + + /// <summary> + /// Occurs when the document property has changed. + /// </summary> + 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); + } + + /// <summary> + /// Recreates the text formatter that is used internally + /// by calling <see cref="TextFormatterFactory.Create"/>. + /// </summary> + void RecreateTextFormatter() + { + if (formatter != null) { + formatter.Dispose(); + formatter = TextFormatterFactory.Create(this); + Redraw(); + } + } + + void RecreateCachedElements() + { + if (cachedElements != null) { + cachedElements.Dispose(); + cachedElements = new TextViewCachedElements(); + } + } + + /// <inheritdoc cref="IWeakEventListener.ReceiveWeakEvent"/> + 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 + /// <summary> + /// Options property. + /// </summary> + public static readonly DependencyProperty OptionsProperty = + DependencyProperty.Register("Options", typeof(TextEditorOptions), typeof(TextView), + new FrameworkPropertyMetadata(OnOptionsChanged)); + + /// <summary> + /// Gets/Sets the options used by the text editor. + /// </summary> + public TextEditorOptions Options { + get { return (TextEditorOptions)GetValue(OptionsProperty); } + set { SetValue(OptionsProperty, value); } + } + + /// <summary> + /// Occurs when a text editor option has changed. + /// </summary> + public event PropertyChangedEventHandler OptionChanged; + + /// <summary> + /// Raises the <see cref="OptionChanged"/> event. + /// </summary> + 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<VisualLineElementGenerator> elementGenerators; + + /// <summary> + /// Gets a collection where element generators can be registered. + /// </summary> + public IList<VisualLineElementGenerator> ElementGenerators { + get { return elementGenerators; } + } + + void ElementGenerator_Added(VisualLineElementGenerator generator) + { + ConnectToTextView(generator); + Redraw(); + } + + void ElementGenerator_Removed(VisualLineElementGenerator generator) + { + DisconnectFromTextView(generator); + Redraw(); + } + + readonly ObserveAddRemoveCollection<IVisualLineTransformer> lineTransformers; + + /// <summary> + /// Gets a collection where line transformers can be registered. + /// </summary> + public IList<IVisualLineTransformer> 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<T>(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; + + /// <summary> + /// Gets the list of layers displayed in the text view. + /// </summary> + 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); + } + + /// <summary> + /// Inserts a new layer at a position specified relative to an existing layer. + /// </summary> + /// <param name="layer">The new layer to insert.</param> + /// <param name="referencedLayer">The existing layer</param> + /// <param name="position">Specifies whether the layer is inserted above,below, or replaces the referenced layer</param> + 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); + } + + /// <inheritdoc/> + protected override int VisualChildrenCount { + get { return layers.Count + inlineObjects.Count; } + } + + /// <inheritdoc/> + 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]; + } + + /// <inheritdoc/> + protected override System.Collections.IEnumerator LogicalChildren { + get { + return inlineObjects.Select(io => io.Element).Concat(layers.Cast<UIElement>()).GetEnumerator(); + } + } + #endregion + + #region Inline object handling + List<InlineObjectRun> inlineObjects = new List<InlineObjectRun>(); + + /// <summary> + /// Adds a new inline object. + /// </summary> + 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<VisualLine> visualLinesWithOutstandingInlineObjects = new List<VisualLine>(); + + 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); + } + } + + /// <summary> + /// Remove the inline objects that were marked for removal. + /// </summary> + 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 + /// <summary> + /// NonPrintableCharacterBrush dependency property. + /// </summary> + public static readonly DependencyProperty NonPrintableCharacterBrushProperty = + DependencyProperty.Register("NonPrintableCharacterBrush", typeof(Brush), typeof(TextView), + new FrameworkPropertyMetadata(Brushes.LightGray)); + + /// <summary> + /// Gets/sets the Brush used for displaying non-printable characters. + /// </summary> + public Brush NonPrintableCharacterBrush { + get { return (Brush)GetValue(NonPrintableCharacterBrushProperty); } + set { SetValue(NonPrintableCharacterBrushProperty, value); } + } + + /// <summary> + /// LinkTextForegroundBrush dependency property. + /// </summary> + public static readonly DependencyProperty LinkTextForegroundBrushProperty = + DependencyProperty.Register("LinkTextForegroundBrush", typeof(Brush), typeof(TextView), + new FrameworkPropertyMetadata(Brushes.Blue)); + + /// <summary> + /// Gets/sets the Brush used for displaying link texts. + /// </summary> + public Brush LinkTextForegroundBrush { + get { return (Brush)GetValue(LinkTextForegroundBrushProperty); } + set { SetValue(LinkTextForegroundBrushProperty, value); } + } + + /// <summary> + /// LinkTextBackgroundBrush dependency property. + /// </summary> + public static readonly DependencyProperty LinkTextBackgroundBrushProperty = + DependencyProperty.Register("LinkTextBackgroundBrush", typeof(Brush), typeof(TextView), + new FrameworkPropertyMetadata(Brushes.Transparent)); + + /// <summary> + /// Gets/sets the Brush used for the background of link texts. + /// </summary> + public Brush LinkTextBackgroundBrush { + get { return (Brush)GetValue(LinkTextBackgroundBrushProperty); } + set { SetValue(LinkTextBackgroundBrushProperty, value); } + } + #endregion + + #region Redraw methods / VisualLine invalidation + /// <summary> + /// Causes the text editor to regenerate all visual lines. + /// </summary> + public void Redraw() + { + Redraw(DispatcherPriority.Normal); + } + + /// <summary> + /// Causes the text editor to regenerate all visual lines. + /// </summary> + public void Redraw(DispatcherPriority redrawPriority) + { + VerifyAccess(); + ClearVisualLines(); + InvalidateMeasure(redrawPriority); + } + + /// <summary> + /// Causes the text editor to regenerate the specified visual line. + /// </summary> + public void Redraw(VisualLine visualLine, DispatcherPriority redrawPriority = DispatcherPriority.Normal) + { + VerifyAccess(); + if (allVisualLines.Remove(visualLine)) { + DisposeVisualLine(visualLine); + InvalidateMeasure(redrawPriority); + } + } + + /// <summary> + /// Causes the text editor to redraw all lines overlapping with the specified segment. + /// </summary> + 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); + } + } + + /// <summary> + /// Causes a known layer to redraw. + /// This method does not invalidate visual lines; + /// use the <see cref="Redraw()"/> method to do that. + /// </summary> + [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); + } + + /// <summary> + /// Causes a known layer to redraw. + /// This method does not invalidate visual lines; + /// use the <see cref="Redraw()"/> method to do that. + /// </summary> + [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); + } + + /// <summary> + /// Causes the text editor to redraw all lines overlapping with the specified segment. + /// Does nothing if segment is null. + /// </summary> + public void Redraw(ISegment segment, DispatcherPriority redrawPriority = DispatcherPriority.Normal) + { + if (segment != null) { + Redraw(segment.Offset, segment.Length, redrawPriority); + } + } + + /// <summary> + /// Invalidates all visual lines. + /// The caller of ClearVisualLines() must also call InvalidateMeasure() to ensure + /// that the visual lines will be recreated. + /// </summary> + 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 + /// <summary> + /// Gets the visual line that contains the document line with the specified number. + /// Returns null if the document line is outside the visible range. + /// </summary> + 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; + } + + /// <summary> + /// 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. + /// </summary> + 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<VisualLine> allVisualLines = new List<VisualLine>(); + ReadOnlyCollection<VisualLine> visibleVisualLines; + double clippedPixelsOnTop; + List<VisualLine> newVisualLines; + + /// <summary> + /// Gets the currently visible visual lines. + /// </summary> + /// <exception cref="VisualLinesInvalidException"> + /// Gets thrown if there are invalid visual lines when this property is accessed. + /// You can use the <see cref="VisualLinesValid"/> property to check for this case, + /// or use the <see cref="EnsureVisualLines()"/> method to force creating the visual lines + /// when they are invalid. + /// </exception> + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations")] + public ReadOnlyCollection<VisualLine> VisualLines { + get { + if (visibleVisualLines == null) + { + return new ReadOnlyCollection<VisualLine>(new List<VisualLine>()); + } + else + { + return visibleVisualLines; + } + } + } + + /// <summary> + /// Gets whether the visual lines are valid. + /// Will return false after a call to Redraw(). + /// Accessing the visual lines property will cause a <see cref="VisualLinesInvalidException"/> + /// if this property is <c>false</c>. + /// </summary> + public bool VisualLinesValid { + get { return visibleVisualLines != null; } + } + + /// <summary> + /// 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. + /// </summary> + public event EventHandler<VisualLineConstructionStartEventArgs> VisualLineConstructionStarting; + + /// <summary> + /// Occurs when the TextView was measured and changed its visual lines. + /// </summary> + public event EventHandler VisualLinesChanged; + + /// <summary> + /// 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. + /// </summary> + /// <exception cref="InvalidOperationException">The visual line build process is already running. + /// It is not allowed to call this method during the construction of a visual line.</exception> + 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); + // UpdateLayout(); + } + //if (!VisualLinesValid) + //throw new VisualLinesInvalidException("Internal error: visual lines invalid after EnsureVisualLines call"); + } + #endregion + + #region Measure + /// <summary> + /// 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. + /// </summary> + const double AdditionalHorizontalScrollAmount = 3; + + Size lastAvailableSize; + bool inMeasure; + + /// <inheritdoc/> + 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<VisualLine>(); + 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)); + } + + /// <summary> + /// Build all VisualLines in the visible range. + /// </summary> + /// <returns>Width the longest line</returns> + 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<VisualLine>(); + + 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<VisualLine>(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<TextLine>(); + 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 + /// <summary> + /// Arrange implementation. + /// </summary> + 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; + } + } + } + InvalidateCursor(); + + return finalSize; + } + #endregion + + #region Render + readonly ObserveAddRemoveCollection<IBackgroundRenderer> backgroundRenderers; + + /// <summary> + /// Gets the list of background renderers. + /// </summary> + public IList<IBackgroundRenderer> BackgroundRenderers { + get { return backgroundRenderers; } + } + + void BackgroundRenderer_Added(IBackgroundRenderer renderer) + { + ConnectToTextView(renderer); + InvalidateLayer(renderer.Layer); + } + + void BackgroundRenderer_Removed(IBackgroundRenderer renderer) + { + DisconnectFromTextView(renderer); + InvalidateLayer(renderer.Layer); + } + + /// <inheritdoc/> + 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<VisualLineDrawingVisual> 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 + /// <summary> + /// Size of the document, in pixels. + /// </summary> + Size scrollExtent; + + /// <summary> + /// Offset of the scroll position. + /// </summary> + Vector scrollOffset; + + /// <summary> + /// Size of the viewport. + /// </summary> + 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; } + } + + /// <summary> + /// Gets the horizontal scroll offset. + /// </summary> + public double HorizontalOffset { + get { return scrollOffset.X; } + } + + /// <summary> + /// Gets the vertical scroll offset. + /// </summary> + public double VerticalOffset { + get { return scrollOffset.Y; } + } + + /// <summary> + /// Gets the scroll offset; + /// </summary> + public Vector ScrollOffset { + get { return scrollOffset; } + } + + /// <summary> + /// Occurs when the scroll offset has changed. + /// </summary> + 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. + + /// <summary> + /// Gets the width of a 'wide space' (the space width used for calculating the tab size). + /// </summary> + /// <remarks> + /// 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. + /// </remarks> + public double WideSpaceWidth { + get { + CalculateDefaultTextMetrics(); + return wideSpaceWidth; + } + } + + /// <summary> + /// 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. + /// </summary> + public double DefaultLineHeight { + get { + CalculateDefaultTextMetrics(); + return defaultLineHeight; + } + } + + /// <summary> + /// Gets the default baseline position. This is the difference between <see cref="VisualYPosition.TextTop"/> + /// and <see cref="VisualYPosition.Baseline"/> for a line containing regular text. + /// Lines that include formatted text or custom UI elements may have a different baseline. + /// </summary> + 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; + } + + /// <summary> + /// Scrolls the text view so that the specified rectangle gets visible. + /// </summary> + 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 + /// <inheritdoc/> + 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; + + /// <summary> + /// Updates the mouse cursor by calling <see cref="Mouse.UpdateCursor"/>, but with input priority. + /// </summary> + public static void InvalidateCursor() + { + if (!invalidCursor) { + invalidCursor = true; + Dispatcher.CurrentDispatcher.BeginInvoke( + DispatcherPriority.Input, + new Action( + delegate { + invalidCursor = false; + Mouse.UpdateCursor(); + })); + } + } + + /// <inheritdoc/> + protected override void OnQueryCursor(QueryCursorEventArgs e) + { + VisualLineElement element = GetVisualLineElementFromPosition(e.GetPosition(this) + scrollOffset); + if (element != null) { + element.OnQueryCursor(e); + } + } + + /// <inheritdoc/> + 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); + } + } + } + + /// <inheritdoc/> + 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 + /// <summary> + /// 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). + /// </summary> + 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; + } + + /// <summary> + /// Gets the visual top position (relative to start of document) from a document line number. + /// </summary> + 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 + /// <summary> + /// Gets the visual position from a text view position. + /// </summary> + /// <param name="position">The text view position.</param> + /// <param name="yPositionMode">The mode how to retrieve the Y position.</param> + /// <returns>The position in WPF device-independent pixels relative + /// to the top left corner of the document.</returns> + 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, yPositionMode); + } + + /// <summary> + /// 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. + /// </summary> + /// <param name="visualPosition">The position in WPF device-independent pixels relative + /// to the top left corner of the document.</param> + /// <returns>The logical position, or null if the position is outside the document.</returns> + public TextViewPosition? GetPosition(Point visualPosition) + { + VerifyAccess(); + if (this.Document == null) + throw ThrowUtil.NoDocumentAssigned(); + VisualLine line = GetVisualLineFromVisualTop(visualPosition.Y); + if (line == null) + return null; + int visualColumn = line.GetVisualColumn(visualPosition); + int documentOffset = line.GetRelativeOffset(visualColumn) + line.FirstDocumentLine.Offset; + return new TextViewPosition(document.GetLocation(documentOffset), visualColumn); + } + + /// <summary> + /// 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. + /// </summary> + /// <param name="visualPosition">The position in WPF device-independent pixels relative + /// to the top left corner of the document.</param> + /// <returns>The logical position, or null if the position is outside the document.</returns> + public TextViewPosition? GetPositionFloor(Point visualPosition) + { + VerifyAccess(); + if (this.Document == null) + throw ThrowUtil.NoDocumentAssigned(); + VisualLine line = GetVisualLineFromVisualTop(visualPosition.Y); + if (line == null) + return null; + int visualColumn = line.GetVisualColumnFloor(visualPosition); + int documentOffset = line.GetRelativeOffset(visualColumn) + line.FirstDocumentLine.Offset; + return new TextViewPosition(document.GetLocation(documentOffset), visualColumn); + } + #endregion + + #region Service Provider + readonly ServiceContainer services = new ServiceContainer(); + + /// <summary> + /// Gets a service container used to associate services with the text view. + /// </summary> + public ServiceContainer Services { + get { return services; } + } + + object IServiceProvider.GetService(Type serviceType) + { + return services.GetService(serviceType); + } + + 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 + /// <summary> + /// The PreviewMouseHover event. + /// </summary> + public static readonly RoutedEvent PreviewMouseHoverEvent = + EventManager.RegisterRoutedEvent("PreviewMouseHover", RoutingStrategy.Tunnel, + typeof(MouseEventHandler), typeof(TextView)); + /// <summary> + /// The MouseHover event. + /// </summary> + public static readonly RoutedEvent MouseHoverEvent = + EventManager.RegisterRoutedEvent("MouseHover", RoutingStrategy.Bubble, + typeof(MouseEventHandler), typeof(TextView)); + + /// <summary> + /// The PreviewMouseHoverStopped event. + /// </summary> + public static readonly RoutedEvent PreviewMouseHoverStoppedEvent = + EventManager.RegisterRoutedEvent("PreviewMouseHoverStopped", RoutingStrategy.Tunnel, + typeof(MouseEventHandler), typeof(TextView)); + /// <summary> + /// The MouseHoverStopped event. + /// </summary> + public static readonly RoutedEvent MouseHoverStoppedEvent = + EventManager.RegisterRoutedEvent("MouseHoverStopped", RoutingStrategy.Bubble, + typeof(MouseEventHandler), typeof(TextView)); + + + /// <summary> + /// Occurs when the mouse has hovered over a fixed location for some time. + /// </summary> + public event MouseEventHandler PreviewMouseHover { + add { AddHandler(PreviewMouseHoverEvent, value); } + remove { RemoveHandler(PreviewMouseHoverEvent, value); } + } + + /// <summary> + /// Occurs when the mouse has hovered over a fixed location for some time. + /// </summary> + public event MouseEventHandler MouseHover { + add { AddHandler(MouseHoverEvent, value); } + remove { RemoveHandler(MouseHoverEvent, value); } + } + + /// <summary> + /// Occurs when the mouse had previously hovered but now started moving again. + /// </summary> + public event MouseEventHandler PreviewMouseHoverStopped { + add { AddHandler(PreviewMouseHoverStoppedEvent, value); } + remove { RemoveHandler(PreviewMouseHoverStoppedEvent, value); } + } + + /// <summary> + /// Occurs when the mouse had previously hovered but now started moving again. + /// </summary> + 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 + + /// <summary> + /// Collapses lines for the purpose of scrolling. <see cref="DocumentLine"/>s marked as collapsed will be hidden + /// and not used to start the generation of a <see cref="VisualLine"/>. + /// </summary> + /// <remarks> + /// This method is meant for <see cref="VisualLineElementGenerator"/>s that cause <see cref="VisualLine"/>s to span + /// multiple <see cref="DocumentLine"/>s. Do not call it without providing a corresponding + /// <see cref="VisualLineElementGenerator"/>. + /// If you want to create collapsible text sections, see <see cref="Folding.FoldingManager"/>. + /// + /// 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 <see cref="CollapsedLineSection.Uncollapse()"/> on the + /// <see cref="CollapsedLineSection"/> returned from this method. + /// </remarks> + public CollapsedLineSection CollapseLines(DocumentLine start, DocumentLine end) + { + VerifyAccess(); + if (heightTree == null) + throw ThrowUtil.NoDocumentAssigned(); + return heightTree.CollapseText(start, end); + } + + /// <summary> + /// Gets the height of the document. + /// </summary> + public double DocumentHeight { + get { + // return 0 if there is no document = no heightTree + return heightTree != null ? heightTree.TotalHeight : 0; + } + } + + /// <summary> + /// Gets the document line at the specified visual position. + /// </summary> + public DocumentLine GetDocumentLineByVisualTop(double visualTop) + { + VerifyAccess(); + if (heightTree == null) + throw ThrowUtil.NoDocumentAssigned(); + return heightTree.GetLineByVisualPosition(visualTop); + } + + /// <inheritdoc/> + 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); + } + } + + /// <summary> + /// The pen used to draw the column ruler. + /// <seealso cref="TextEditorOptions.ShowColumnRuler"/> + /// </summary> + 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; + } + + /// <summary> + /// Gets/Sets the pen used to draw the column ruler. + /// <seealso cref="TextEditorOptions.ShowColumnRuler"/> + /// </summary> + public Pen ColumnRulerPen { + get { return (Pen)GetValue(ColumnRulerPenProperty); } + set { SetValue(ColumnRulerPenProperty, value); } + } + } +} |
