From 0513c24aafa0504ba6bd04fcb825ef5def83996a Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Mon, 7 Feb 2011 22:12:47 +0100 Subject: [PATCH] Squashed 'AvalonEdit/' changes from 58e2044..307ad08 307ad08 Merge branch 'master' of f:\SD.git-bundle 05ebf8f Merge branch '4.0' a0901d5 Improved inline object handling. Inline objects that change their size in response to user input are now supported. be409d9 AvalonEdit: Change inline object implementation so that inline objects are direct children of the TextView (instead of being children of the TextLayer). 38e5797 move MouseHover logic from TextView to separate class and make it reusable git-subtree-dir: AvalonEdit git-subtree-split: 307ad0829ce305b08e3266f4165822eb1fc5288d --- .../ICSharpCode.AvalonEdit.csproj | 1 + .../Rendering/InlineObjectRun.cs | 15 +- .../Rendering/MouseHoverLogic.cs | 102 ++++++++ ICSharpCode.AvalonEdit/Rendering/TextLayer.cs | 89 +------ ICSharpCode.AvalonEdit/Rendering/TextView.cs | 244 ++++++++++++------ .../Rendering/VisualLine.cs | 1 + .../Rendering/VisualLineTextSource.cs | 3 +- 7 files changed, 287 insertions(+), 168 deletions(-) create mode 100644 ICSharpCode.AvalonEdit/Rendering/MouseHoverLogic.cs diff --git a/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj b/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj index d572f6c8b..8484dd233 100644 --- a/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj +++ b/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj @@ -257,6 +257,7 @@ TextView.cs + FormattedTextElement.cs diff --git a/ICSharpCode.AvalonEdit/Rendering/InlineObjectRun.cs b/ICSharpCode.AvalonEdit/Rendering/InlineObjectRun.cs index b3c76914e..70e940ed0 100644 --- a/ICSharpCode.AvalonEdit/Rendering/InlineObjectRun.cs +++ b/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/ICSharpCode.AvalonEdit/Rendering/MouseHoverLogic.cs b/ICSharpCode.AvalonEdit/Rendering/MouseHoverLogic.cs new file mode 100644 index 000000000..3c3a70c48 --- /dev/null +++ b/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/ICSharpCode.AvalonEdit/Rendering/TextLayer.cs b/ICSharpCode.AvalonEdit/Rendering/TextLayer.cs index beae3ba49..e2b06e337 100644 --- a/ICSharpCode.AvalonEdit/Rendering/TextLayer.cs +++ b/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/ICSharpCode.AvalonEdit/Rendering/TextView.cs b/ICSharpCode.AvalonEdit/Rendering/TextView.cs index 1a67c34bd..6fdaa077a 100644 --- a/ICSharpCode.AvalonEdit/Rendering/TextView.cs +++ b/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/ICSharpCode.AvalonEdit/Rendering/VisualLine.cs b/ICSharpCode.AvalonEdit/Rendering/VisualLine.cs index 29ff6c162..47744a594 100644 --- a/ICSharpCode.AvalonEdit/Rendering/VisualLine.cs +++ b/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/ICSharpCode.AvalonEdit/Rendering/VisualLineTextSource.cs b/ICSharpCode.AvalonEdit/Rendering/VisualLineTextSource.cs index fa6fa6f1c..eea016446 100644 --- a/ICSharpCode.AvalonEdit/Rendering/VisualLineTextSource.cs +++ b/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; }