|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Windows.Media; // Brush, Transform
using System.Windows.Media.Animation; // AnimationClock
using System.Windows.Controls; // ScrollViewer
using MS.Win32; // SafeNativeMethods
using MS.Internal; // DoubleUtil.AreClose(), Invariant.Assert
using MS.Internal.Documents; // IFlowDocumentViewer
using System.Runtime.InteropServices; // HandleRef
using System.Windows.Interop;
using System.Windows.Controls.Primitives;
//
// Description: Caret rendering visual.
//
namespace System.Windows.Documents
{
// Disable pragma warnings to enable PREsharp pragmas
#pragma warning disable 1634, 1691
/// <summary>
/// This class is sealed because it calls OnVisualChildrenChanged virtual in the
/// constructor and it does not override it, but derived classes could.
/// </summary>
internal sealed class CaretElement : Adorner
{
//------------------------------------------------------
//
// Constructors
//
//------------------------------------------------------
#region Constructors
/// <summary>
/// Creates new instance of CaretElement.
/// </summary>
/// <param name="textEditor">
/// TextEditor that owns this Adorner.
/// </param>
/// <param name="isBlinkEnabled">
/// Blinking for caret animation. Drag target caret does not need blinking,
/// </param>
internal CaretElement(TextEditor textEditor, bool isBlinkEnabled) : base(textEditor.TextView.RenderScope)
{
Invariant.Assert(textEditor.TextView != null && textEditor.TextView.RenderScope != null, "Assert: textView != null && RenderScope != null");
_textEditor = textEditor;
// Set the animation whether do it or not.
_isBlinkEnabled = isBlinkEnabled;
// caret position
_left = 0.0;
_top = 0.0;
// caret dimensions
_systemCaretWidth = SystemParameters.CaretWidth;
_height = 0.0;
// Set AllowDropProperty as "False" not to inherit the value from the ancestor.
AllowDrop = false;
_caretElement = new CaretSubElement();
_caretElement.ClipToBounds = false;
AddVisualChild(_caretElement);
}
#endregion Constructors
//------------------------------------------------------
//
// Public Methods
//
//------------------------------------------------------
#region Public Methods
#endregion Public Methods
//------------------------------------------------------
//
// Protected Methods
//
//------------------------------------------------------
#region Protected Methods
/// <summary>
/// Returns the Visual children count.
/// </summary>
protected override int VisualChildrenCount
{
get { return (_caretElement == null) ? 0 : 1; }
}
/// <summary>
/// Returns the child at the specified index.
/// </summary>
protected override Visual GetVisualChild(int index)
{
if (index != 0)
{
throw new ArgumentOutOfRangeException("index", index, SR.Visual_ArgumentOutOfRange);
}
return _caretElement;
}
// HitTestCore override not to hit testable Caret.
protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
{
// Return null not to hit testable for CaretElement.
return null;
}
// Render override -- we render the selection here.
protected override void OnRender(DrawingContext drawingContext)
{
if (_selectionGeometry != null)
{
FrameworkElement owner = GetOwnerElement();
Brush selectionBrush = (Brush)owner.GetValue(TextBoxBase.SelectionBrushProperty);
if (selectionBrush == null)
{
return;
}
double selectionOpacity = (double)owner.GetValue(TextBoxBase.SelectionOpacityProperty);
drawingContext.PushOpacity(selectionOpacity);
Pen selectionOutlinePen = null;
drawingContext.DrawGeometry(selectionBrush, selectionOutlinePen, _selectionGeometry);
drawingContext.Pop();
}
}
/// <summary>
/// Measurement override.
/// </summary>
/// <param name="availableSize">
/// Available size for the component
/// </param>
/// <returns>
/// Return the size of the caret
/// </returns>
protected override Size MeasureOverride(Size availableSize)
{
base.MeasureOverride(availableSize);
_caretElement.InvalidateVisual();
// Return the available width and height. Please don't
// return AdornedElement.RenderSize since it will be scrolled
// in case of the reder size is greater than the available size.
// Reference bug#1068444.
// We choose to return an arbitrary large value of double.MaxValue/2 in case of infinite height/width.
// This is safer, because adding (even zero) margin to double.MaxValue causes available size to become infinite again.
// UIElement.Measure enforces that MeasureCore can not return PositiveInfinity size even if given Infinite available size.
return new Size(
double.IsInfinity(availableSize.Width) ? double.MaxValue/2 : availableSize.Width,
double.IsInfinity(availableSize.Height) ? double.MaxValue/2 : availableSize.Height);
// Note we do not use _systemCaretWidth for caret width,
// because italic caret would be clipped in this case.
// We use maximum available visible width - as for height.
}
/// <summary>
/// Arrange override.
/// </summary>
/// <param name="availableSize">
/// Available size for the component
/// </param>
/// <returns>
/// Return the size of the caret
/// </returns>
protected override Size ArrangeOverride(Size availableSize)
{
Point point;
if (_pendingGeometryUpdate)
{
((TextSelection)_textEditor.Selection).UpdateCaretState(CaretScrollMethod.None);
_pendingGeometryUpdate = false;
}
point = new Point(_left, _top);
_caretElement.Arrange(new Rect(point, availableSize));
return availableSize;
}
#endregion Protected Events
//------------------------------------------------------
//
// Internal Methods
//
//------------------------------------------------------
#region Internal Methods
/// <summary>
/// Invalidates the caret render.
/// Used in TextSelection (for caret) and in TextEditor (for drag target)
/// </summary>
/// <param name="visible">
/// </param>
/// <param name="caretRectangle">
/// Rectangle relative to AdornedElement (textview) where caret should be drawn.
/// Width must be zero to apply the system caret width except the interim caret that has
/// the interim caret width.
/// </param>
/// <param name="caretBrush">
/// Brush for drawing the caret
/// </param>
/// <param name="opacity">
/// Opacity factor to apply to RenderContext when drawing the caret.
/// </param>
/// <param name="italic">
/// Request to italic caret.
/// </param>
/// <param name="scrollMethod">
/// Determines how the caret is scrolled into view.
/// </param>
/// <param name="scrollToOriginPosition">
/// Request to scroll caret position with the scroll origin position.
/// </param>
internal void Update(bool visible, Rect caretRectangle, Brush caretBrush, double opacity, bool italic, CaretScrollMethod scrollMethod, double scrollToOriginPosition)
{
double newLeft;
double newTop;
double newHeight;
double newWidth;
bool positionChanged;
Invariant.Assert(caretBrush != null, "Assert: caretBrush != null");
// Make sure we're attached to the view. We have to delay this work
// until now because if we swap out a render scope on the fly, the
// new one won't be hooked up to the visual tree until after its
// style is rebuilt, which happens on an async layout update.
EnsureAttachedToView();
// Enforce caret refresh for the case when it appears after invisible state
bool justAppearing = visible && !_showCaret;
if (_showCaret != visible)
{
InvalidateVisual();
_showCaret = visible;
}
// Update the caret brush.
_caretBrush = caretBrush;
_opacity = opacity;
// Define new coordinates and dimensions of the caret.
// We don't consider caret visibility here because even if the
// caret is hidden, we need to calc the geometry info to scroll
// the active edge of the selection into view.
if (caretRectangle.IsEmpty || caretRectangle.Height <= 0)
{
newLeft = 0;
newTop = 0;
newHeight = 0;
newWidth = 0;
}
else
{
newLeft = caretRectangle.X;
newTop = caretRectangle.Y;
newHeight = caretRectangle.Height;
newWidth = SystemParameters.CaretWidth;
}
// Initialize flag requiring to refresh the caret
positionChanged = justAppearing || italic != _italic;
if (!DoubleUtil.AreClose(_left, newLeft))
{
_left = newLeft;
positionChanged = true;
}
if (!DoubleUtil.AreClose(_top, newTop))
{
_top = newTop;
positionChanged = true;
}
if (!caretRectangle.IsEmpty && _interimWidth != caretRectangle.Width)
{
_interimWidth = caretRectangle.Width;
positionChanged = true;
}
if (!DoubleUtil.AreClose(_systemCaretWidth, newWidth))
{
_systemCaretWidth = newWidth;
positionChanged = true;
}
if (!DoubleUtil.AreClose(_height, newHeight))
{
_height = newHeight;
InvalidateMeasure();
}
// Refresh caret and ensure the caret to the view if the caret position is changed or
// caret is currently out of view area which scrollToOriginPosition is set.
// scrollToOriginPosition will be set properly to view the caret correctly if caret is
// currently out of view boundary. For example, typing bidi characters on LTR flow direction
// or typing western characters on RTL flow direction from the out of view.
if (positionChanged || !double.IsNaN(scrollToOriginPosition))
{
_scrolledToCurrentPositionYet = false;
RefreshCaret(italic);
}
if (scrollMethod != CaretScrollMethod.None && !_scrolledToCurrentPositionYet)
{
Rect scrollRectangle;
//Set the interim width to show the interim caret in the interim mode.
// We're providing enough space to also take care of an italic or bidi caret.
scrollRectangle = new Rect(_left - CaretPaddingWidth, _top, CaretPaddingWidth * 2 + (IsInInterimState ? _interimWidth : _systemCaretWidth), _height);
// If we're scrolling to one edge of the document or another,
// - Going backward, we want to scroll into view the left edge
// of the rect. This is the default.
// - Going forward, we want to scroll into view the right edge
// of the rect, which takes some adjustment.
if (!double.IsNaN(scrollToOriginPosition) && scrollToOriginPosition > 0)
{
scrollRectangle.X += scrollRectangle.Width;
scrollRectangle.Width = 0;
}
switch (scrollMethod)
{
case CaretScrollMethod.Simple:
DoSimpleScrollToView(scrollToOriginPosition, scrollRectangle);
break;
case CaretScrollMethod.Navigation:
DoNavigationalScrollToView(scrollToOriginPosition, scrollRectangle);
break;
}
_scrolledToCurrentPositionYet = true;
}
// Skip the animation if the animation isn't set. E.g. DragDrop caret.
SetBlinkAnimation(visible, positionChanged);
}
// Scroll the caret rect into view with no allowance for surrounding text.
private void DoSimpleScrollToView(double scrollToOriginPosition, Rect scrollRectangle)
{
// Scroll to the original position first to ensure of displaying the wrapped word with
// the caret position. The scrollToOriginPosition is already base on both LTR and RTL
// flow direction.
if (!double.IsNaN(scrollToOriginPosition))
{
MS.Internal.Documents.TextViewBase.BringRectIntoViewMinimally(_textEditor.TextView, new Rect(scrollToOriginPosition, scrollRectangle.Y, scrollRectangle.Width, scrollRectangle.Height));
// Since we've moved the viewport, and scrollRectangle is relative to the viewport. scrollRectangle
// is no longer correct. Adjust it by the distance we scrolled to make it correct.
scrollRectangle.X -= scrollToOriginPosition;
}
// Now scroll to the caret position
MS.Internal.Documents.TextViewBase.BringRectIntoViewMinimally(_textEditor.TextView, scrollRectangle);
}
// Scroll the caret rect into view, adding a "buffer" of surrounding text
// porportional to the viewport size.
private void DoNavigationalScrollToView(double scrollToOriginPosition, Rect targetRect)
{
// Find the scroller from the render scope
ScrollViewer scroller = _textEditor._Scroller as ScrollViewer;
if (scroller != null)
{
Point targetPoint = new Point(targetRect.Left, targetRect.Top);
GeneralTransform transform = _textEditor.TextView.RenderScope.TransformToAncestor(scroller);
if (transform.TryTransform(targetPoint, out targetPoint))
{
double scrollerWidth = scroller.ViewportWidth;
double scrollerHeight = scroller.ViewportHeight;
double deltaToScroll;
// Scroll up or down if the moving position.Y is outside of viewport
if (targetPoint.Y < 0 || targetPoint.Y + targetRect.Height > scrollerHeight)
{
if (targetPoint.Y < 0)
{
// Scroll up
deltaToScroll = Math.Abs(targetPoint.Y);
scroller.ScrollToVerticalOffset(Math.Max(0, scroller.VerticalOffset - deltaToScroll - scrollerHeight / 4));
}
else
{
// Scroll down
deltaToScroll = targetPoint.Y + targetRect.Height - scrollerHeight;
scroller.ScrollToVerticalOffset(Math.Min(scroller.ExtentHeight, scroller.VerticalOffset + deltaToScroll + scrollerHeight / 4));
}
}
// Scroll line left or right if the moving position.X is outside of viewport
if (targetPoint.X < 0 || targetPoint.X > scrollerWidth)
{
if (targetPoint.X < 0)
{
// Scroll the left line
deltaToScroll = Math.Abs(targetPoint.X);
scroller.ScrollToHorizontalOffset(Math.Max(0, scroller.HorizontalOffset - deltaToScroll - scrollerWidth / 4));
}
else
{
// Scroll the right line
deltaToScroll = targetPoint.X - scrollerWidth;
scroller.ScrollToHorizontalOffset(Math.Min(scroller.ExtentWidth, scroller.HorizontalOffset + deltaToScroll + scrollerWidth / 4));
}
}
}
}
else
{
// No heuristics implemented for document viewer horizontal scrolling,
// as a result we will bring each character or word into view on demand.
// (Unlike the scrollviewer heuristic above where we bring 1/4th of scroller width into view.)
if (!_textEditor.Selection.MovingPosition.HasValidLayout && _textEditor.TextView != null && _textEditor.TextView.IsValid)
{
DoSimpleScrollToView(scrollToOriginPosition, targetRect);
}
}
}
/// <summary>
/// Updates selection geometry.
/// </summary>
internal void UpdateSelection()
{
Geometry previousSelectionGeometry = _selectionGeometry;
_selectionGeometry = null;
if (!_textEditor.Selection.IsEmpty)
{
EnsureAttachedToView();
List<TextSegment> textSegments = _textEditor.Selection.TextSegments;
for (int i = 0; i < textSegments.Count; i++)
{
TextSegment segment = textSegments[i];
Geometry geometry = _textEditor.Selection.TextView.GetTightBoundingGeometryFromTextPositions(segment.Start, segment.End);
AddGeometry(ref _selectionGeometry, geometry);
}
}
if (_selectionGeometry != previousSelectionGeometry)
{
// Request to re-render the selection.
RefreshCaret(_italic);
}
}
internal static void AddGeometry(ref Geometry geometry, Geometry addedGeometry)
{
if (addedGeometry != null)
{
if (geometry == null)
{
geometry = addedGeometry;
}
else
{
geometry = Geometry.Combine(geometry, addedGeometry, GeometryCombineMode.Union, null, CaretElement.c_geometryCombineTolerance, ToleranceType.Absolute);
}
}
}
internal static void ClipGeometryByViewport(ref Geometry geometry, Rect viewport)
{
if (geometry != null)
{
Geometry viewportGeometry = new RectangleGeometry(viewport);
geometry = Geometry.Combine(geometry, viewportGeometry, GeometryCombineMode.Intersect, null, CaretElement.c_geometryCombineTolerance, ToleranceType.Absolute);
}
}
internal static void AddTransformToGeometry(Geometry targetGeometry, Transform transformToAdd)
{
if (targetGeometry != null && transformToAdd != null)
{
targetGeometry.Transform = (targetGeometry.Transform == null || targetGeometry.Transform.IsIdentity)
? transformToAdd
: new MatrixTransform(targetGeometry.Transform.Value * transformToAdd.Value);
}
}
/// <summary>
/// Hide the caret render.
/// </summary>
internal void Hide()
{
if (_showCaret)
{
// Hide the caret not to render the caret.
_showCaret = false;
InvalidateVisual();
// Skip the animation if the animation isn't set. E.g. DragDrop caret.
SetBlinking(/*isBlinkEnabled:*/false);
// Destroy Win32 caret
Win32DestroyCaret();
}
}
/// <summary>
/// Requires to visually invalidate and update a caret without calculating
/// its size and coordinates (assuming that they are the same as in preceding Update).
/// Used to force caret redraing when input language or italic condition is changed.
/// Also called internally from Update method after new position and size is
/// calculated and set to the caret.
/// </summary>
/// <param name="italic">
/// True specifies that the caret must be inclined to indicate italic state.
/// </param>
internal void RefreshCaret(bool italic)
{
// Store italic status of the caret
_italic = italic;
// Cache _adornerLayer to avoid reentrancy problems during Update.
AdornerLayer adornerLayer = _adornerLayer;
if (adornerLayer != null)
{
Adorner[] adorners = adornerLayer.GetAdorners(this.AdornedElement);
if (adorners != null)
{
// Verify we still adorn our element.
// We have a persistent but still unexplained stress bug where
// the caret adorner is mysteriously detached
for (int i = 0; i < adorners.Length; i++)
{
if (adorners[i] == this)
{
adornerLayer.Update(this.AdornedElement);
adornerLayer.InvalidateVisual();
break;
}
}
}
}
}
// Removes this CaretElement from its AdornerLayer.
internal void DetachFromView()
{
SetBlinking(/*isBlinkEnabled:*/false);
if (_adornerLayer != null)
{
_adornerLayer.Remove(this);
_adornerLayer = null;
}
}
internal void SetBlinking(bool isBlinkEnabled)
{
if (isBlinkEnabled != _isBlinkEnabled)
{
// Stopping blinking
if (_isBlinkEnabled)
{
if (_blinkAnimationClock != null)
{
if (_blinkAnimationClock.CurrentState == ClockState.Active)
{
_blinkAnimationClock.Controller.Stop();
}
}
}
// Actual blinking will be started in Update method call - if needed
_isBlinkEnabled = isBlinkEnabled;
if (isBlinkEnabled)
{
// Create Win32 caret.
Win32CreateCaret();
}
else
{
// Destory Win32 caret.
Win32DestroyCaret();
}
}
}
internal void UpdateCaretBrush(Brush caretBrush)
{
_caretBrush = caretBrush;
_caretElement.InvalidateVisual();
}
/// <summary>
/// Performs the actual rendering of the caret on the given context. Called by
/// CaretSubElement.
/// </summary>
/// <param name="context">Drawing context</param>
/// <remarks>This method is on CaretElement instead of CaretSubElement because CaretElement
/// knows all of the necessary data, and conceptually CaretSubElement only exists to provide
/// a rendering surface.</remarks>
internal void OnRenderCaretSubElement(DrawingContext context)
{
// Sync up Win32 caret position with Avalon caret position.
Win32SetCaretPos();
if (_showCaret)
{
TextEditorThreadLocalStore threadLocalStore = TextEditor._ThreadLocalStore;
Invariant.Assert(!(_italic && this.IsInInterimState), "Assert !(_italic && IsInInterimState)");
// Drawing context's pushed count to pop it up
int contextPushedCount = 0;
// Apply internally requested opacity.
context.PushOpacity(_opacity);
contextPushedCount++;
// Apply italic transformation
if (_italic && !(threadLocalStore.Bidi))
{
// Rotate transform 20 degree for italic that is the based on 'H' italic degree.
// NOTE: The angle of italic caret is constant. This is Word behavior
// established after usability studies with conditional angle dependent
// on font properties - they discovered that variations look annoying.
// NOTE: We ignore _italic setting in _bidi case. This is Word behavior.
// When flow direction is Right to Left, we need to reverse the caret transform.
//
// Get the flow direction which is the flow direction of AdornedElement.
// CaretElement is rendering the caret that based on AdornedElement, so we can
// render the right italic caret whatever the text content set the flow direction.
FlowDirection flowDirection = (FlowDirection)AdornedElement.GetValue(FlowDirectionProperty);
context.PushTransform(new RotateTransform(
flowDirection == FlowDirection.RightToLeft ? -20 : 20,
0, _height));
contextPushedCount++;
}
if (this.IsInInterimState || _systemCaretWidth > DefaultNarrowCaretWidth)
{
// Make the block caret partially transparent to avoid obstructing text.
context.PushOpacity(CaretOpacity);
contextPushedCount++;
}
if (this.IsInInterimState)
{
// Render the interim block caret as the specified interim block caret width.
context.DrawRectangle(_caretBrush, null, new Rect(0, 0, _interimWidth, _height));
}
else
{
// Snap the caret to device pixels.
if (!_italic || threadLocalStore.Bidi)
{
GuidelineSet guidelineSet = new GuidelineSet(new double[] { -(_systemCaretWidth / 2), _systemCaretWidth / 2 }, null);
context.PushGuidelineSet(guidelineSet);
contextPushedCount++;
}
// If we don't snap, the caret will render as a 2 pixel wide rect, one pixel in each bordering char bounding box.
context.DrawRectangle(_caretBrush, null, new Rect(-(_systemCaretWidth / 2), 0, _systemCaretWidth, _height));
}
if (threadLocalStore.Bidi)
{
// Set the Bidi caret indicator width. TextBox/RichTextBox control must have
// the enough margin to display BiDi indicator.
double bidiCaretIndicatorWidth = BidiCaretIndicatorWidth;
// Get the flow direction which is the flow direction of AdornedElement.
// Because CaretElement is rendering the caret that based on AdornedElement.
// With getting the flow direction, we can render the BiDi caret indicator correctly
// whatever AdornedElement's flow direction is set.
FlowDirection flowDirection = (FlowDirection)AdornedElement.GetValue(FlowDirectionProperty);
if (flowDirection == FlowDirection.RightToLeft)
{
// BiDi caret indicator should always direct by the right to left
bidiCaretIndicatorWidth = bidiCaretIndicatorWidth * (-1);
}
// Draw BIDI caret to indicate the coming input is BIDI characters.
// Shape is a little flag oriented to the left - as in Word.
// Orientation does not depend on anything (which seems to be Word behavior).
// Confirm that flag orientation is constant in all cases.
PathGeometry pathGeometry;
PathFigure pathFigure;
pathGeometry = new PathGeometry();
pathFigure = new PathFigure();
pathFigure.StartPoint = new Point(0, 0);
pathFigure.Segments.Add(new LineSegment(new Point(-bidiCaretIndicatorWidth, 0), true));
pathFigure.Segments.Add(new LineSegment(new Point(0, _height / BidiIndicatorHeightRatio), true));
pathFigure.IsClosed = true;
pathGeometry.Figures.Add(pathFigure);
context.DrawGeometry(_caretBrush, null, pathGeometry);
}
// Pop the drawing context if pushed for italic or opacity setting
for (int i = 0; i < contextPushedCount; i++)
{
context.Pop();
}
}
else
{
// Destroy Win32 caret.
Win32DestroyCaret();
}
}
// ITextView.Updated event listener. Called by the TextSelection.
internal void OnTextViewUpdated()
{
// At this point, we potentially have to update our selection geometry,
// but the TextView is still dirty.
//
// We cannot simply delay the work with a Dispatcher.BeginInvoke,
// because that will introduce latency (flicker) as the selection/caret
// update lags behind the text refresh.
//
// So we invalidate this Adorner's arrange so that we can re-evaluate
// in the same layout pass as the view update, after this method returns.
_pendingGeometryUpdate = true;
InvalidateArrange();
}
// ------------------------------------------------------------
//
// Internal Properties for Test Automation
//
// ------------------------------------------------------------
// This method is used for text automation in DrtEditing
private static CaretElement Debug_CaretElement
{
get
{
TextEditorThreadLocalStore threadLocalStore = TextEditor._ThreadLocalStore;
return ((ITextSelection)TextEditor._ThreadLocalStore.FocusedTextSelection).CaretElement;
}
}
// This method is used for text automation in DrtEditing
private static FrameworkElement Debug_RenderScope
{
get
{
return ((ITextSelection)TextEditor._ThreadLocalStore.FocusedTextSelection).TextView.RenderScope as FrameworkElement; // TextBlock / TextFlow
}
}
#endregion Internal methods
//------------------------------------------------------
//
// Internal Properties
//
//------------------------------------------------------
#region Internal Properties
internal Geometry SelectionGeometry
{
get
{
return _selectionGeometry;
}
}
internal bool IsSelectionActive
{
get
{
return _isSelectionActive;
}
set
{
_isSelectionActive = value;
}
}
#endregion Internal Properties
//------------------------------------------------------
//
// Private Methods
//
//------------------------------------------------------
#region Private Methods
// Returns the outer element that owns this CaretElement. (One of the text boxes
// or FlowDocument readers.)
private FrameworkElement GetOwnerElement()
{
return GetOwnerElement(_textEditor.UiScope);
}
internal static FrameworkElement GetOwnerElement(FrameworkElement uiScope)
{
if (uiScope is IFlowDocumentViewer)
{
// We are in one of the internal FlowDocument viewers owned by FlowDocumentReader.
// Return the FlowDocumentReader since this class holds the public propertie values.
DependencyObject node = uiScope;
while (node != null)
{
if (node is FlowDocumentReader)
{
return (FrameworkElement)node;
}
node = VisualTreeHelper.GetParent(node);
}
return null;
}
return uiScope;
}
// Adds this CaretElement to the scoping AdornerLayer, if any.
private void EnsureAttachedToView()
{
AdornerLayer layer = AdornerLayer.GetAdornerLayer(_textEditor.TextView.RenderScope);
if (layer == null)
{
// There is no AdornerLayer available. Clear cached value and exit.
if (_adornerLayer != null)
{
// We're currently in a layer that doesn't exist.
_adornerLayer.Remove(this);
}
_adornerLayer = null;
return;
}
if (_adornerLayer == layer)
{
// We're already using the correct AdornerLayer.
return;
}
if (_adornerLayer != null)
{
// We're currently in the wrong layer.
_adornerLayer.Remove(this);
}
// Add ourselves to the correct layer.
_adornerLayer = layer;
_adornerLayer.Add(this, ZOrderValue);
}
// Inits or resets an opacity animation used to make the caret blink.
private void SetBlinkAnimation(bool visible, bool positionChanged)
{
if (!_isBlinkEnabled)
{
return;
}
// NB: "Blink" is the period of time between successive
// state changes in the caret animation. "Flash" is
// 2 * blink, the period of time to transition from
// visible, to hidden, to visible again.
int blinkInterval = Win32GetCaretBlinkTime();
if (blinkInterval > 0) // -1 if the caret shouldn't blink.
{
Duration blinkDuration = new Duration(TimeSpan.FromMilliseconds(blinkInterval * 2));
if (_blinkAnimationClock == null || _blinkAnimationClock.Timeline.Duration != blinkDuration)
{
DoubleAnimationUsingKeyFrames blinkAnimation = new DoubleAnimationUsingKeyFrames();
blinkAnimation.BeginTime = null;
blinkAnimation.RepeatBehavior = RepeatBehavior.Forever;
blinkAnimation.KeyFrames.Add(new DiscreteDoubleKeyFrame(1, KeyTime.FromPercent(0.0)));
blinkAnimation.KeyFrames.Add(new DiscreteDoubleKeyFrame(0, KeyTime.FromPercent(0.5)));
blinkAnimation.Duration = blinkDuration;
// Reduce desired framerate from 60 to 10, to reduce number of no-op renders.
// This significantly improves typing responsiveness on low end GPUs for RichTextBox.
Timeline.SetDesiredFrameRate(blinkAnimation, 10);
_blinkAnimationClock = blinkAnimation.CreateClock();
_blinkAnimationClock.Controller.Begin();
_caretElement.ApplyAnimationClock(UIElement.OpacityProperty, _blinkAnimationClock);
}
}
else if (_blinkAnimationClock != null)
{
// No blink (control panel option).
_caretElement.ApplyAnimationClock(UIElement.OpacityProperty, null);
_blinkAnimationClock = null;
}
if (_blinkAnimationClock != null)
{
// Disable the animation when the caret isn't rendered
// to get better perf.
if (visible && (!(_blinkAnimationClock.CurrentState == ClockState.Active) || positionChanged))
{
// Start the blinking animation for caret
_blinkAnimationClock.Controller.Begin();
}
else if (!visible)
{
// Stop the blinking animation if the caret isn't visible even we start it yet.
// Because ApplyAnimationClock() calling can start the animation without
// the calling of Begin() which case is the auto selection(with DatePicker).
_blinkAnimationClock.Controller.Stop();
}
}
}
// Create Win32 caret to sync up Avalon caret with Win32 application and show Win32 caret
// which is the empty bitmap. This call will generate the accessiblity event for caret so that
// Win32 application have the compatibility to handle the caret event which is Magnifier or Tablet Tip.
private void Win32CreateCaret()
{
if (!_isSelectionActive)
{
// We do not want to interfere with Win32 caret
// if this Adorner isnt representing active selection
return;
}
// Create Win32 caret if the height of caret is changed or
// doesn't exist Win32 caret.
if (!_win32Caret || _win32Height != _height)
{
// Create Win32 caret to support Win32 application(like Magnifier) that want to
// sync Avalon caret position.
IntPtr hwnd = IntPtr.Zero;
PresentationSource source = PresentationSource.CriticalFromVisual(this);
if (source != null)
{
hwnd = (source as IWin32Window).Handle;
}
if (hwnd != IntPtr.Zero)
{
// Convert _height (fixed at 96 dpi) to device units.
double deviceHeight = source.CompositionTarget.TransformToDevice.Transform(new Point(0, _height)).Y;
// Win32 CreateCaret automatically destroys the previous caret shape,
// if any, regardless of the window that owns the caret.
// Create and show Win32 empty caret for win32 compatibility.
// Creating Win32 empty caret and show it will generate the accesibility event
// so that Win32 application will have the compatibility who listen the caret event.
// Specified height with the current caret height will sync the win32 caret which
// Win32 Magnifer rely on the caret height to scroll their window.
NativeMethods.BitmapHandle bitmap = UnsafeNativeMethods.CreateBitmap(/*width*/ 1, /*height*/ ConvertToInt32(deviceHeight), /*panels*/ 1, /*bitsPerPixel*/ 1, /*bits*/ null);
// Specified width and height as zero since they will be ignored by setting the bitmap.
bool returnValue = UnsafeNativeMethods.CreateCaret(new HandleRef(null, hwnd), bitmap, /*width*/ 0, /*height*/ 0);
int win32Error = Marshal.GetLastWin32Error();
if (returnValue)
{
_win32Caret = true;
_win32Height = _height;
}
else
{
_win32Caret = false;
// Throw the Win32 exception with GetLastWin32Error.
throw new System.ComponentModel.Win32Exception(win32Error);
}
}
}
}
// Destroy Win32 caret if we create it with checking Win32 error.
private void Win32DestroyCaret()
{
if (!_isSelectionActive)
{
// We do not want to interfere with Win32 caret
// if this Adorner isnt representing active selection
return;
}
// We only destroy the caret what we created a Win32 caret.
if (_win32Caret)
{
// Destroy Win32 caret.
bool returnValue = SafeNativeMethods.DestroyCaret();
int win32Error = Marshal.GetLastWin32Error();
if (!returnValue)
{
// Suppress Win32 error of failing DestroyCaret(), because CreateCaret()can
// automatically destroys caret resource, if any, regardless of the owns caret.
// Mshtml module can create Win32 caret when Avalon is loaded by IE, so our Win32
// caret resousce is already destroyed.
//throw new System.ComponentModel.Win32Exception(win32Error);
}
_win32Caret = false;
_win32Height = 0;
}
}
// Set Win32 caret position with checking Win32 error.
private void Win32SetCaretPos()
{
if (!_isSelectionActive)
{
// We do not want to interfere with Win32 caret
// if this Adorner isnt representing active selection
return;
}
// Create Win32 caret if win32 caret isn't created yet or destroyed already.
if (!_win32Caret)
{
Win32CreateCaret();
}
// Get the presentation source to find a root visual.
PresentationSource source = null;
source = PresentationSource.CriticalFromVisual(this);
if (source != null)
{
// Calculate the current caret position then transform the point to the window's client position
// so that Win32 applicatioin like as Magnifer(MSAA) or Tablet Ink icon can track the right
// caret position by calling Win32 SetCaretPos that will generate MSAA event message
// as EVENT_OBJECT_LOCATIONCHANGE.
Point win32CaretPoint = new Point(0, 0);
GeneralTransform transform = _caretElement.TransformToAncestor(source.RootVisual);
if (!transform.TryTransform(win32CaretPoint, out win32CaretPoint))
{
// Reset the win32 caret point as zero if TryTransform is failed
win32CaretPoint = new Point(0, 0);
}
// Convert visual point (fixed at 96 dpi) to device units.
win32CaretPoint = source.CompositionTarget.TransformToDevice.Transform(win32CaretPoint);
bool win32Return = SafeNativeMethods.SetCaretPos(ConvertToInt32(win32CaretPoint.X), ConvertToInt32(win32CaretPoint.Y));
// GetLastWin32Error() have to be called immediately not to break FxCop rule
// even though we ignore the first fail of SetCaretPos.
int win32Error = Marshal.GetLastWin32Error();
if (!win32Return)
{
// Suppress the first chance fail of Win32 SetCaretPos. Because Win32 caret can be
// created by Win32 application(like as mshtml.dll) that destroying our avalon Win32
// caret resource to update Avalon caret position to the system. So we try to create
// our own Win32 caret resource here and try to set the caret position again.
// Throw the exception if we still have a fail of SetCaretPos on the second time.
_win32Caret = false;
Win32CreateCaret();
win32Return = SafeNativeMethods.SetCaretPos(ConvertToInt32(win32CaretPoint.X), ConvertToInt32(win32CaretPoint.Y));
win32Error = Marshal.GetLastWin32Error();
if (!win32Return)
{
// Throw the Win32 exception with GetLastWin32Error.
throw new System.ComponentModel.Win32Exception(win32Error);
}
}
}
}
// Converts a double into a 32 bit integer, truncating values that
// exceed Int32.MinValue or Int32.MaxValue.
private int ConvertToInt32(double value)
{
int i;
if (double.IsNaN(value))
{
i = 0;
}
else if (value < Int32.MinValue)
{
i = Int32.MinValue;
}
else if (value > Int32.MaxValue)
{
i = Int32.MaxValue;
}
else
{
i = Convert.ToInt32(value);
}
return i;
}
// Get the system caret blink time with checking Win32 error.
private int Win32GetCaretBlinkTime()
{
Invariant.Assert(_isSelectionActive, "Blink animation should only be required for an owner with active selection.");
// Disable PreSharp#6523 - Win32 GetCaretBlinkTime can return "0"
// without the error if SetCaretBlinkTime set as "0".
#pragma warning disable 6523
int caretBlinkTime = (int)SafeNativeMethods.GetCaretBlinkTime();
if (caretBlinkTime == 0)
{
// Return "-1" which is no blinking caret instead of throwing
// exception.
return -1;
}
#pragma warning restore 6523
return caretBlinkTime;
}
#endregion Private methods
//------------------------------------------------------
//
// Private Properties
//
//------------------------------------------------------
#region Private Properties
private bool IsInInterimState
{
get
{
// Returns true if the interim width is specified and shows
// the interim block caret.
return _interimWidth != 0;
}
}
#endregion Private Properties
//------------------------------------------------------
//
// Internal Fields
//
//------------------------------------------------------
#region Internal Fields
// BiDi caret indicator width.
internal const double BidiCaretIndicatorWidth = 2.0;
// Caret padding width to ensure the visible caret for Bidi and Italic.
// Control(TextBox/RichTextBox) must have the enough padding to display
// BiDi and Italic caret indicator.
internal const double CaretPaddingWidth = 5.0;
#endregion Internal Fields
//------------------------------------------------------
//
// Private Types
//
//------------------------------------------------------
#region Private Types
private class CaretSubElement : UIElement
{
internal CaretSubElement()
{
}
// HitTestCore override not to hit testable Caret.
protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
{
// Return null not to hit testable for CaretElement.
return null;
}
protected override void OnRender(DrawingContext drawingContext)
{
((CaretElement)_parent).OnRenderCaretSubElement(drawingContext);
}
}
#endregion Private Types
//------------------------------------------------------
//
// Private Fields
//
//------------------------------------------------------
#region Private Fields
// TextEditor that owns this adorner.
private readonly TextEditor _textEditor;
// If false, the caret is hidden and should not be rendered.
private bool _showCaret;
// If false, we dont interfere with Win32 Caret
private bool _isSelectionActive;
// Our blink opacity animation.
private AnimationClock _blinkAnimationClock;
// left offset of caret
private double _left;
// top offset of caret
private double _top;
// width of caret
private double _systemCaretWidth;
// width of interim caret
private double _interimWidth;
// height of caret
private double _height;
// height of Win32 caret
private double _win32Height;
// flag to control animation.
private bool _isBlinkEnabled;
// caret brush.
private Brush _caretBrush;
// opacity factor to apply to RenderContext when drawing caret.
private double _opacity;
// AdornerLayer holding this caret visual.
private AdornerLayer _adornerLayer;
// italic caret
private bool _italic;
// win32 caret
private bool _win32Caret;
// Caret opacity percent
private const double CaretOpacity = 0.5;
// BiDi caret indicator height ratio of caret
private const double BidiIndicatorHeightRatio = 10.0;
// default narrow caret width
private const double DefaultNarrowCaretWidth = 1.0;
// selection related data
private Geometry _selectionGeometry;
internal const double c_geometryCombineTolerance = 1e-4;
internal const double c_endOfParaMagicMultiplier = 0.5;
// ZOrder
internal const int ZOrderValue = System.Int32.MaxValue / 2;
// child element on which we render the caret (selection is rendered on the adorner itself)
private readonly CaretSubElement _caretElement;
// Flag set true after calls to OnTextViewUpdated.
// When true, we need to re-evaluate the selection geometry on the
// next Arrange.
private bool _pendingGeometryUpdate;
// Flag set when we scroll and cleared when the position changes.
// Used in Update() to optimize when we scroll.
private bool _scrolledToCurrentPositionYet;
#endregion Private Fields
}
}
|