diff --git a/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj b/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj index dc000bbf5..05f481a78 100644 --- a/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj +++ b/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj @@ -255,6 +255,7 @@ TextView.cs + FormattedTextElement.cs diff --git a/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/InlineObjectRun.cs b/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/InlineObjectRun.cs index b3c76914e..70e940ed0 100644 --- a/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/InlineObjectRun.cs +++ b/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/InlineObjectRun.cs @@ -38,11 +38,6 @@ namespace ICSharpCode.AvalonEdit.Rendering if (context == null) throw new ArgumentNullException("context"); - // remove inline object if its already added, can happen e.g. when recreating textrun for word-wrapping - // TODO: certainly the text view should handle this internally? external code might want to use InlineObjectRun, - // but doesn't have access to textLayer.RemoveInlineObject - context.TextView.textLayer.RemoveInlineObject(this.Element); - return new InlineObjectRun(1, this.TextRunProperties, this.Element); } } @@ -55,6 +50,7 @@ namespace ICSharpCode.AvalonEdit.Rendering UIElement element; int length; TextRunProperties properties; + internal Size desiredSize; /// /// Creates a new InlineObjectRun instance. @@ -122,11 +118,10 @@ namespace ICSharpCode.AvalonEdit.Rendering /// public override TextEmbeddedObjectMetrics Format(double remainingParagraphWidth) { - Size size = element.DesiredSize; double baseline = TextBlock.GetBaselineOffset(element); if (double.IsNaN(baseline)) - baseline = size.Height; - return new TextEmbeddedObjectMetrics(size.Width, size.Height, baseline); + baseline = desiredSize.Height; + return new TextEmbeddedObjectMetrics(desiredSize.Width, desiredSize.Height, baseline); } /// @@ -135,8 +130,8 @@ namespace ICSharpCode.AvalonEdit.Rendering if (this.element.IsArrangeValid) { double baseline = TextBlock.GetBaselineOffset(element); if (double.IsNaN(baseline)) - baseline = element.DesiredSize.Height; - return new Rect(new Point(0, -baseline), element.DesiredSize); + baseline = desiredSize.Height; + return new Rect(new Point(0, -baseline), desiredSize); } else { return Rect.Empty; } diff --git a/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/MouseHoverLogic.cs b/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/MouseHoverLogic.cs new file mode 100644 index 000000000..3c3a70c48 --- /dev/null +++ b/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/MouseHoverLogic.cs @@ -0,0 +1,102 @@ +// 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.Windows; +using System.Windows.Input; +using System.Windows.Threading; + +namespace ICSharpCode.AvalonEdit.Rendering +{ + public class MouseHoverLogic : IDisposable + { + UIElement target; + + DispatcherTimer mouseHoverTimer; + Point mouseHoverStartPoint; + MouseEventArgs mouseHoverLastEventArgs; + bool mouseHovering; + + public MouseHoverLogic(UIElement target) + { + if (target == null) + throw new ArgumentNullException("target"); + this.target = target; + this.target.MouseLeave += MouseHoverLogicMouseLeave; + this.target.MouseMove += MouseHoverLogicMouseMove; + } + + void MouseHoverLogicMouseMove(object sender, MouseEventArgs e) + { + Point newPosition = e.GetPosition(this.target); + Vector mouseMovement = mouseHoverStartPoint - newPosition; + if (Math.Abs(mouseMovement.X) > SystemParameters.MouseHoverWidth + || Math.Abs(mouseMovement.Y) > SystemParameters.MouseHoverHeight) + { + StopHovering(); + mouseHoverStartPoint = newPosition; + mouseHoverLastEventArgs = e; + mouseHoverTimer = new DispatcherTimer(SystemParameters.MouseHoverTime, DispatcherPriority.Background, + OnMouseHoverTimerElapsed, this.target.Dispatcher); + mouseHoverTimer.Start(); + } + // do not set e.Handled - allow others to also handle MouseMove + } + + void MouseHoverLogicMouseLeave(object sender, MouseEventArgs e) + { + StopHovering(); + // do not set e.Handled - allow others to also handle MouseLeave + } + + void StopHovering() + { + if (mouseHoverTimer != null) { + mouseHoverTimer.Stop(); + mouseHoverTimer = null; + } + if (mouseHovering) { + mouseHovering = false; + OnMouseHoverStopped(mouseHoverLastEventArgs); + } + } + + void OnMouseHoverTimerElapsed(object sender, EventArgs e) + { + mouseHoverTimer.Stop(); + mouseHoverTimer = null; + + mouseHovering = true; + OnMouseHover(mouseHoverLastEventArgs); + } + + public event EventHandler MouseHover; + + protected virtual void OnMouseHover(MouseEventArgs e) + { + if (MouseHover != null) { + MouseHover(this, e); + } + } + + public event EventHandler MouseHoverStopped; + + protected virtual void OnMouseHoverStopped(MouseEventArgs e) + { + if (MouseHoverStopped != null) { + MouseHoverStopped(this, e); + } + } + + bool disposed; + + public void Dispose() + { + if (!disposed) { + this.target.MouseLeave -= MouseHoverLogicMouseLeave; + this.target.MouseMove -= MouseHoverLogicMouseMove; + } + disposed = true; + } + } +} diff --git a/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/TextLayer.cs b/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/TextLayer.cs index beae3ba49..e2b06e337 100644 --- a/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/TextLayer.cs +++ b/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/TextLayer.cs @@ -24,6 +24,11 @@ namespace ICSharpCode.AvalonEdit.Rendering /// sealed class TextLayer : Layer { + /// + /// the index of the text layer in the layers collection + /// + internal int index; + public TextLayer(TextView textView) : base(textView, KnownLayer.Text) { } @@ -33,89 +38,5 @@ namespace ICSharpCode.AvalonEdit.Rendering base.OnRender(drawingContext); textView.RenderTextLayer(drawingContext); } - - #region Inline object handling - internal List inlineObjects = new List(); - - /// - /// Adds a new inline object. - /// - internal void AddInlineObject(InlineObjectRun inlineObject) - { - inlineObjects.Add(inlineObject); - AddVisualChild(inlineObject.Element); - inlineObject.Element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); - } - - List visualLinesWithOutstandingInlineObjects = new List(); - - internal 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. - visualLinesWithOutstandingInlineObjects.Add(visualLine); - } - - internal void RemoveInlineObjectsNow() - { - inlineObjects.RemoveAll( - ior => { - if (visualLinesWithOutstandingInlineObjects.Contains(ior.VisualLine)) { - RemoveInlineObjectRun(ior); - return true; - } - return false; - }); - visualLinesWithOutstandingInlineObjects.Clear(); - } - - // Remove InlineObjectRun.Element from TextLayer. - // Caller of RemoveInlineObjectRun will remove it from inlineObjects collection. - void RemoveInlineObjectRun(InlineObjectRun ior) - { - if (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 = textView; - while (element != null && !element.Focusable) { - element = VisualTreeHelper.GetParent(element) as UIElement; - } - if (element != null) - Keyboard.Focus(element); - } - ior.VisualLine = null; - RemoveVisualChild(ior.Element); - } - - /// - /// Removes the inline object that displays the specified UIElement. - /// - internal void RemoveInlineObject(UIElement element) - { - inlineObjects.RemoveAll( - ior => { - if (ior.Element == element) { - RemoveInlineObjectRun(ior); - return true; - } - return false; - }); - } - - /// - protected override int VisualChildrenCount { - get { return inlineObjects.Count; } - } - - /// - protected override Visual GetVisualChild(int index) - { - return inlineObjects[index].Element; - } - #endregion } } diff --git a/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/TextView.cs b/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/TextView.cs index 1a67c34bd..6fdaa077a 100644 --- a/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/TextView.cs +++ b/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/TextView.cs @@ -53,8 +53,12 @@ namespace ICSharpCode.AvalonEdit.Rendering this.Options = new TextEditorOptions(); Debug.Assert(singleCharacterElementGenerator != null); // assert that the option change created the builtin element generators - layers = new UIElementCollection(this, this); + 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 @@ -294,7 +298,7 @@ namespace ICSharpCode.AvalonEdit.Rendering #region Layers internal readonly TextLayer textLayer; - readonly UIElementCollection layers; + readonly LayerCollection layers; /// /// Gets the list of layers displayed in the text view. @@ -303,6 +307,47 @@ namespace ICSharpCode.AvalonEdit.Rendering 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. /// @@ -352,18 +397,128 @@ namespace ICSharpCode.AvalonEdit.Rendering /// protected override int VisualChildrenCount { - get { return layers.Count; } + get { return layers.Count + inlineObjects.Count; } } /// protected override Visual GetVisualChild(int index) { - return layers[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 layers.GetEnumerator(); } + 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 @@ -410,7 +565,6 @@ namespace ICSharpCode.AvalonEdit.Rendering { VerifyAccess(); if (allVisualLines.Remove(visualLine)) { - visibleVisualLines = null; DisposeVisualLine(visualLine); InvalidateMeasure(redrawPriority); } @@ -422,7 +576,6 @@ namespace ICSharpCode.AvalonEdit.Rendering public void Redraw(int offset, int length, DispatcherPriority redrawPriority = DispatcherPriority.Normal) { VerifyAccess(); - bool removedLine = false; bool changedSomethingBeforeOrInLine = false; for (int i = 0; i < allVisualLines.Count; i++) { VisualLine visualLine = allVisualLines[i]; @@ -431,15 +584,11 @@ namespace ICSharpCode.AvalonEdit.Rendering if (offset <= lineEnd) { changedSomethingBeforeOrInLine = true; if (offset + length >= lineStart) { - removedLine = true; allVisualLines.RemoveAt(i--); DisposeVisualLine(visualLine); } } } - if (removedLine) { - visibleVisualLines = null; - } 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. @@ -492,11 +641,12 @@ namespace ICSharpCode.AvalonEdit.Rendering if (newVisualLines != null && newVisualLines.Contains(visualLine)) { throw new ArgumentException("Cannot dispose visual line because it is in construction!"); } + visibleVisualLines = null; visualLine.IsDisposed = true; foreach (TextLine textLine in visualLine.TextLines) { textLine.Dispose(); } - textLayer.RemoveInlineObjects(visualLine); + RemoveInlineObjects(visualLine); } #endregion @@ -677,11 +827,11 @@ namespace ICSharpCode.AvalonEdit.Rendering ClearVisualLines(); lastAvailableSize = availableSize; - textLayer.RemoveInlineObjectsNow(); - foreach (UIElement layer in layers) { layer.Measure(availableSize); } + MeasureInlineObjects(); + InvalidateVisual(); // = InvalidateArrange+InvalidateRender textLayer.InvalidateVisual(); @@ -700,7 +850,8 @@ namespace ICSharpCode.AvalonEdit.Rendering } } - textLayer.RemoveInlineObjectsNow(); + // 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; @@ -949,7 +1100,7 @@ namespace ICSharpCode.AvalonEdit.Rendering foreach (var span in textLine.GetTextRunSpans()) { InlineObjectRun inline = span.Value as InlineObjectRun; if (inline != null && inline.VisualLine != null) { - Debug.Assert(textLayer.inlineObjects.Contains(inline)); + 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)); } @@ -1566,63 +1717,12 @@ namespace ICSharpCode.AvalonEdit.Rendering remove { RemoveHandler(MouseHoverStoppedEvent, value); } } - DispatcherTimer mouseHoverTimer; - Point mouseHoverStartPoint; - MouseEventArgs mouseHoverLastEventArgs; - bool mouseHovering; + MouseHoverLogic hoverLogic; - /// - protected override void OnMouseMove(MouseEventArgs e) + void RaiseHoverEventPair(MouseEventArgs e, RoutedEvent tunnelingEvent, RoutedEvent bubblingEvent) { - base.OnMouseMove(e); - Point newPosition = e.GetPosition(this); - Vector mouseMovement = mouseHoverStartPoint - newPosition; - if (Math.Abs(mouseMovement.X) > SystemParameters.MouseHoverWidth - || Math.Abs(mouseMovement.Y) > SystemParameters.MouseHoverHeight) - { - StopHovering(); - mouseHoverStartPoint = newPosition; - mouseHoverLastEventArgs = e; - mouseHoverTimer = new DispatcherTimer(SystemParameters.MouseHoverTime, DispatcherPriority.Background, - OnMouseHoverTimerElapsed, this.Dispatcher); - mouseHoverTimer.Start(); - } - // do not set e.Handled - allow others to also handle MouseMove - } - - /// - protected override void OnMouseLeave(MouseEventArgs e) - { - base.OnMouseLeave(e); - StopHovering(); - // do not set e.Handled - allow others to also handle MouseLeave - } - - void StopHovering() - { - if (mouseHoverTimer != null) { - mouseHoverTimer.Stop(); - mouseHoverTimer = null; - } - if (mouseHovering) { - mouseHovering = false; - RaiseHoverEventPair(PreviewMouseHoverStoppedEvent, MouseHoverStoppedEvent); - } - } - - void OnMouseHoverTimerElapsed(object sender, EventArgs e) - { - mouseHoverTimer.Stop(); - mouseHoverTimer = null; - - mouseHovering = true; - RaiseHoverEventPair(PreviewMouseHoverEvent, MouseHoverEvent); - } - - void RaiseHoverEventPair(RoutedEvent tunnelingEvent, RoutedEvent bubblingEvent) - { - var mouseDevice = mouseHoverLastEventArgs.MouseDevice; - var stylusDevice = mouseHoverLastEventArgs.StylusDevice; + var mouseDevice = e.MouseDevice; + var stylusDevice = e.StylusDevice; int inputTime = Environment.TickCount; var args1 = new MouseEventArgs(mouseDevice, inputTime, stylusDevice) { RoutedEvent = tunnelingEvent, @@ -1636,8 +1736,6 @@ namespace ICSharpCode.AvalonEdit.Rendering }; RaiseEvent(args2); } - - #endregion /// diff --git a/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLine.cs b/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLine.cs index 29ff6c162..47744a594 100644 --- a/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLine.cs +++ b/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLine.cs @@ -22,6 +22,7 @@ namespace ICSharpCode.AvalonEdit.Rendering { TextView textView; List elements; + internal bool hasInlineObjects; /// /// Gets the document to which this VisualLine belongs. diff --git a/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLineTextSource.cs b/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLineTextSource.cs index fa6fa6f1c..eea016446 100644 --- a/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLineTextSource.cs +++ b/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/VisualLineTextSource.cs @@ -42,7 +42,8 @@ namespace ICSharpCode.AvalonEdit.Rendering InlineObjectRun inlineRun = run as InlineObjectRun; if (inlineRun != null) { inlineRun.VisualLine = VisualLine; - TextView.textLayer.AddInlineObject(inlineRun); + VisualLine.hasInlineObjects = true; + TextView.AddInlineObject(inlineRun); } return run; }