| File: MS\Internal\Ink\Renderer.cs | Web Access |
| Project: src\wpf\src\Microsoft.DotNet.Wpf\src\PresentationCore\PresentationCore.csproj (PresentationCore) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Windows.Media; using MS.Internal.Ink; using MS.Internal.PresentationCore; namespace System.Windows.Ink { /// <summary> /// The Renderer class is used to render a stroke collection. /// This class listens to stroke added and removed events on the /// stroke collection and updates the internal visual tree. /// It also listens to the Invalidated event on Stroke (fired when /// DrawingAttributes changes, packet data changes, DrawingAttributes /// replaced, as well as Stroke developer calls Stroke.OnInvalidated) /// and updates the visual state as necessary. /// </summary> /// internal class Renderer { #region StrokeVisual /// <summary> /// A retained visual for rendering a single stroke in a stroke collection view /// </summary> private class StrokeVisual : System.Windows.Media.DrawingVisual { /// <summary> /// Constructor /// </summary> /// <param name="stroke">a stroke to render into this visual</param> /// <param name="renderer">a renderer associated to this visual</param> internal StrokeVisual(Stroke stroke, Renderer renderer) : base() { Debug.Assert(renderer != null); ArgumentNullException.ThrowIfNull(stroke); _stroke = stroke; _renderer = renderer; // The original value of the color and IsHighlighter are cached so // when Stroke.Invalidated is fired, Renderer knows whether re-arranging // the visual tree is needed. _cachedColor = stroke.DrawingAttributes.Color; _cachedIsHighlighter = stroke.DrawingAttributes.IsHighlighter; // Update the visual contents Update(); } /// <summary> /// The Stroke rendedered into this visual. /// </summary> internal Stroke Stroke { get { return _stroke; } } /// <summary> /// Updates the contents of the visual. /// </summary> internal void Update() { using (DrawingContext drawingContext = RenderOpen()) { bool highContrast = _renderer.IsHighContrast(); if (highContrast && _stroke.DrawingAttributes.IsHighlighter) { // we don't render highlighters in high contrast return; } DrawingAttributes da; if (highContrast) { da = _stroke.DrawingAttributes.Clone(); da.Color = _renderer.GetHighContrastColor(); } else if (_stroke.DrawingAttributes.IsHighlighter) { // Get the drawing attributes to use for a highlighter stroke. This can be a copied DA with color.A // overridden if color.A != 255. da = StrokeRenderer.GetHighlighterAttributes(_stroke, _stroke.DrawingAttributes); } else { // Otherwise, usethe DA on this stroke da = _stroke.DrawingAttributes; } // Draw selected stroke as hollow _stroke.DrawInternal (drawingContext, da, _stroke.IsSelected ); } } /// <summary> /// StrokeVisual should not be hittestable as it interferes with event routing /// </summary> protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParams) { return null; } /// <summary> /// The previous value of IsHighlighter /// </summary> internal bool CachedIsHighlighter { get {return _cachedIsHighlighter;} set {_cachedIsHighlighter = value;} } /// <summary> /// The previous value of Color /// </summary> internal Color CachedColor { get {return _cachedColor;} set {_cachedColor = value;} } private Stroke _stroke; private bool _cachedIsHighlighter; private Color _cachedColor; private Renderer _renderer; } /// <summary> /// Private helper that helps reverse map the highlighter dictionary /// </summary> private class HighlighterContainerVisual : ContainerVisual { internal HighlighterContainerVisual(Color color) { _color = color; } /// <summary> /// The Color of the strokes in this highlighter container visual /// </summary> internal Color Color { get { return _color; } } private Color _color; } #endregion #region Internal interface /// <summary> /// Public Constructor /// </summary> internal Renderer() { // Initialize the data members. // We intentionally don't use lazy initialization for the core members to avoid // hidden bug situations when one thing has been created while another not. // If the user is looking for lazy initialization, she should do that on her // own and create Renderer only when there's a need for it. // Create visuals that'll be the containers for all other visuals // created by the Renderer. This visuals are created once and are // not supposed to be replaced nor destroyed while the Renderer is alive. _rootVisual = new ContainerVisual(); _highlightersRoot = new ContainerVisual(); _regularInkVisuals = new ContainerVisual(); _incrementalRenderingVisuals = new ContainerVisual(); // Highlighters go to the bottom, then regular ink, and regular // ink' incremental rendering in on top. VisualCollection rootChildren = _rootVisual.Children; rootChildren.Add(_highlightersRoot); rootChildren.Add(_regularInkVisuals); rootChildren.Add(_incrementalRenderingVisuals); // Set the default value of highcontrast to be false. _highContrast = false; // Create a stroke-visual dictionary _visuals = new Dictionary<Stroke, StrokeVisual>(); } /// <summary> /// Returns a reference to a visual tree that can be used to render the ink. /// This property may be either a single visual or a container visual with /// children. The element uses this visual as a child visual and arranges /// it with respects to the other siblings. /// Note: No visuals are actually generated until the application gets /// this property for the first time. If no strokes are set then an empty /// visual is returned. /// </summary> internal Visual RootVisual { get { return _rootVisual; } } /// <summary> /// Set the strokes property to the collection of strokes to be rendered. /// The Renderer will then listen to changes to the StrokeCollection /// and update its state to reflect the changes. /// </summary> internal StrokeCollection Strokes { get { // We should never return a null value. if ( _strokes == null ) { _strokes = new StrokeCollection(); // Start listening on events from the stroke collection. _strokes.StrokesChangedInternal += new StrokeCollectionChangedEventHandler(OnStrokesChanged); } return _strokes; } set { ArgumentNullException.ThrowIfNull(value); if (value == _strokes) { return; } // Detach the current stroke collection if (null != _strokes) { // Stop listening on events from the stroke collection. _strokes.StrokesChangedInternal -= new StrokeCollectionChangedEventHandler(OnStrokesChanged); foreach (StrokeVisual visual in _visuals.Values) { StopListeningOnStrokeEvents(visual.Stroke); // Detach the visual from the tree DetachVisual(visual); } _visuals.Clear(); } // Set it. _strokes = value; // Create visuals foreach (Stroke stroke in _strokes) { // Create a visual per stroke StrokeVisual visual = new StrokeVisual(stroke, this); // Store the stroke-visual pair in the dictionary _visuals.Add(stroke, visual); StartListeningOnStrokeEvents(visual.Stroke); // Attach it to the visual tree AttachVisual(visual, buildingStrokeCollection: true); } // Start listening on events from the stroke collection. _strokes.StrokesChangedInternal += new StrokeCollectionChangedEventHandler(OnStrokesChanged); } } /// <summary> /// User supposed to use this method to attach IncrementalRenderer's root visual /// to the visual tree of a stroke collection view. /// </summary> /// <param name="visual">visual to attach</param> /// <param name="drawingAttributes">drawing attributes that used in the incremental rendering</param> internal void AttachIncrementalRendering(Visual visual, DrawingAttributes drawingAttributes) { // Check the input parameters ArgumentNullException.ThrowIfNull(visual); ArgumentNullException.ThrowIfNull(drawingAttributes); //harden against eaten exceptions bool exceptionRaised = false; // Verify that the visual hasn't been attached already if (_attachedVisuals != null) { foreach(Visual alreadyAttachedVisual in _attachedVisuals) { if (visual == alreadyAttachedVisual) { exceptionRaised = true; throw new System.InvalidOperationException(SR.CannotAttachVisualTwice); } } } else { // Create the list to register attached visuals in _attachedVisuals = new List<Visual>(); } if (!exceptionRaised) { // The position of the visual in the tree depends on the drawingAttributes // Find the appropriate parent visual to attach this visual to. ContainerVisual parent = drawingAttributes.IsHighlighter ? GetContainerVisual(drawingAttributes) : _incrementalRenderingVisuals; // Attach the visual to the tree parent.Children.Add(visual); // Put the visual into the list of visuals attached via this method _attachedVisuals.Add(visual); } } /// <summary> /// Detaches a visual previously attached via AttachIncrementalRendering /// </summary> /// <param name="visual">the visual to detach</param> internal void DetachIncrementalRendering(Visual visual) { ArgumentNullException.ThrowIfNull(visual); // Remove the visual in the list of attached via AttachIncrementalRendering if ((_attachedVisuals == null) || (!_attachedVisuals.Remove(visual))) { throw new System.InvalidOperationException(SR.VisualCannotBeDetached); } // Detach it from the tree DetachVisual(visual); } /// <summary> /// Internal helper used to indicate if a visual was previously attached /// via a call to AttachIncrementalRendering /// </summary> internal bool ContainsAttachedIncrementalRenderingVisual(Visual visual) { if (visual == null || _attachedVisuals == null) { return false; } return _attachedVisuals.Contains(visual); } /// <summary> /// Internal helper used to determine if a visual is in the right spot in the visual tree /// </summary> internal bool AttachedVisualIsPositionedCorrectly(Visual visual, DrawingAttributes drawingAttributes) { if (visual == null || drawingAttributes == null || _attachedVisuals == null || !_attachedVisuals.Contains(visual)) { return false; } ContainerVisual correctParent = drawingAttributes.IsHighlighter ? GetContainerVisual(drawingAttributes) : _incrementalRenderingVisuals; ContainerVisual currentParent = VisualTreeHelper.GetParent(visual) as ContainerVisual; if (currentParent == null || correctParent != currentParent) { return false; } return true; } /// <summary> /// TurnOnHighContrast turns on the HighContrast rendering mode /// </summary> /// <param name="strokeColor">The stroke color under high contrast</param> internal void TurnHighContrastOn(Color strokeColor) { if ( !_highContrast || strokeColor != _highContrastColor ) { _highContrast = true; _highContrastColor = strokeColor; UpdateStrokeVisuals(); } } /// <summary> /// ResetHighContrast turns off the HighContrast mode /// </summary> internal void TurnHighContrastOff() { if ( _highContrast ) { _highContrast = false; UpdateStrokeVisuals(); } } /// <summary> /// Indicates whether the renderer is in high contrast mode. /// </summary> /// <returns></returns> internal bool IsHighContrast() { return _highContrast; } /// <summary> /// returns the stroke color for the high contrast rendering. /// </summary> /// <returns></returns> public Color GetHighContrastColor() { return _highContrastColor; } #endregion #region Event handlers /// <summary> /// StrokeCollectionChanged event handler /// </summary> private void OnStrokesChanged(object sender, StrokeCollectionChangedEventArgs eventArgs) { System.Diagnostics.Debug.Assert(sender == _strokes); // Read the args StrokeCollection added = eventArgs.Added; StrokeCollection removed = eventArgs.Removed; // Add new strokes foreach (Stroke stroke in added) { // Verify that it's not a dupe if (_visuals.ContainsKey(stroke)) { throw new System.ArgumentException(SR.DuplicateStrokeAdded); } // Create a visual for the new stroke and add it to the dictionary StrokeVisual visual = new StrokeVisual(stroke, this); _visuals.Add(stroke, visual); // Start listening on the stroke events StartListeningOnStrokeEvents(visual.Stroke); // Attach it to the visual tree AttachVisual(visual, buildingStrokeCollection: false); } // Deal with removed strokes first foreach (Stroke stroke in removed) { // Verify that the event is in sync with the view StrokeVisual visual = null; if (_visuals.TryGetValue(stroke, out visual)) { // get rid of both the visual and the stroke DetachVisual(visual); StopListeningOnStrokeEvents(visual.Stroke); _visuals.Remove(stroke); } else { throw new System.ArgumentException(SR.UnknownStroke3); } } } /// <summary> /// Stroke Invalidated event handler /// </summary> private void OnStrokeInvalidated(object sender, EventArgs eventArgs) { System.Diagnostics.Debug.Assert(_strokes.IndexOf(sender as Stroke) != -1); // Find the visual associated with the changed stroke. StrokeVisual visual; Stroke stroke = (Stroke)sender; if (!_visuals.TryGetValue(stroke, out visual)) { throw new System.ArgumentException(SR.UnknownStroke1); } // The original value of IsHighligher and Color are cached in StrokeVisual. // if (IsHighlighter value changed or (IsHighlighter == true and not changed and color changed) // detach and re-attach the corresponding visual; // otherwise Invalidate the corresponding StrokeVisual if (visual.CachedIsHighlighter != stroke.DrawingAttributes.IsHighlighter || (stroke.DrawingAttributes.IsHighlighter && StrokeRenderer.GetHighlighterColor(visual.CachedColor) != StrokeRenderer.GetHighlighterColor(stroke.DrawingAttributes.Color))) { // The change requires reparenting the visual in the tree. DetachVisual(visual); AttachVisual(visual, buildingStrokeCollection: false); // Update the cached values visual.CachedIsHighlighter = stroke.DrawingAttributes.IsHighlighter; visual.CachedColor = stroke.DrawingAttributes.Color; } // Update the visual. visual.Update(); } #endregion #region Helper methods /// <summary> /// Update the stroke visuals /// </summary> private void UpdateStrokeVisuals() { foreach ( StrokeVisual strokeVisual in _visuals.Values ) { strokeVisual.Update(); } } /// <summary> /// Attaches a stroke visual to the tree based on the stroke's /// drawing attributes and/or its z-order (index in the collection). /// </summary> private void AttachVisual(StrokeVisual visual, bool buildingStrokeCollection) { System.Diagnostics.Debug.Assert(_strokes != null); if (visual.Stroke.DrawingAttributes.IsHighlighter) { // Find or create a container visual for highlighter strokes of the color ContainerVisual parent = GetContainerVisual(visual.Stroke.DrawingAttributes); Debug.Assert(visual is not null); //insert StrokeVisuals under any non-StrokeVisuals used for dynamic inking int i = 0; for (int j = parent.Children.Count - 1; j >= 0; j--) { if (parent.Children[j] is StrokeVisual) { i = j + 1; break; } } parent.Children.Insert(i, visual); } else { // For regular ink we have to respect the z-order of the strokes. // The implementation below is not optimal in a generic case, but the // most simple and should work ok in most common scenarios. // Find the nearest non-highlighter stroke with a lower z-order // and insert the new visual right next to the visual of that stroke. StrokeVisual precedingVisual = null; int i = 0; if (buildingStrokeCollection) { Stroke visualStroke = visual.Stroke; //we're building up a stroke collection, no need to start at IndexOf, i = Math.Min(_visuals.Count, _strokes.Count); //not -1, we're about to decrement while (--i >= 0) { if (object.ReferenceEquals(_strokes[i], visualStroke)) { break; } } } else { i = _strokes.IndexOf(visual.Stroke); } while (--i >= 0) { Stroke stroke = _strokes[i]; if ((!stroke.DrawingAttributes.IsHighlighter) && (_visuals.TryGetValue(stroke, out precedingVisual)) && (VisualTreeHelper.GetParent(precedingVisual) != null)) { VisualCollection children = ((ContainerVisual)(VisualTreeHelper.GetParent(precedingVisual))).Children; int index = children.IndexOf(precedingVisual); children.Insert(index + 1, visual); break; } } // If found no non-highlighter strokes with a lower z-order, insert // the stroke at the very bottom of the regular ink visual tree. if (i < 0) { ContainerVisual parent = GetContainerVisual(visual.Stroke.DrawingAttributes); parent.Children.Insert(0, visual); } } } /// <summary> /// Detaches a visual from the tree, also removes highligher parents if empty /// when true is passed /// </summary> private void DetachVisual(Visual visual) { ContainerVisual parent = (ContainerVisual)(VisualTreeHelper.GetParent(visual)); if (parent != null) { VisualCollection children = parent.Children; children.Remove(visual); // If the parent is a childless highlighter, detach it too. HighlighterContainerVisual hcVisual = parent as HighlighterContainerVisual; if (hcVisual != null && hcVisual.Children.Count == 0 && _highlighters != null && _highlighters.ContainsValue(hcVisual)) { DetachVisual(hcVisual); _highlighters.Remove(hcVisual.Color); } } } /// <summary> /// Attaches event handlers to stroke events /// </summary> private void StartListeningOnStrokeEvents(Stroke stroke) { System.Diagnostics.Debug.Assert(stroke != null); stroke.Invalidated += new EventHandler(OnStrokeInvalidated); } /// <summary> /// Detaches event handlers from stroke /// </summary> private void StopListeningOnStrokeEvents(Stroke stroke) { System.Diagnostics.Debug.Assert(stroke != null); stroke.Invalidated -= new EventHandler(OnStrokeInvalidated); } /// <summary> /// Finds a container for a new visual based on the drawing attributes /// of the stroke rendered into that visual. /// </summary> /// <param name="drawingAttributes">drawing attributes</param> /// <returns>visual</returns> private ContainerVisual GetContainerVisual(DrawingAttributes drawingAttributes) { System.Diagnostics.Debug.Assert(drawingAttributes != null); HighlighterContainerVisual hcVisual; if (drawingAttributes.IsHighlighter) { // For a highlighter stroke, the color.A is neglected. Color color = StrokeRenderer.GetHighlighterColor(drawingAttributes.Color); if ((_highlighters == null) || (!_highlighters.TryGetValue(color, out hcVisual))) { if (_highlighters == null) { _highlighters = new Dictionary<Color, HighlighterContainerVisual>(); } hcVisual = new HighlighterContainerVisual(color) { Opacity = StrokeRenderer.HighlighterOpacity }; _highlightersRoot.Children.Add(hcVisual); _highlighters.Add(color, hcVisual); } else if (VisualTreeHelper.GetParent(hcVisual) == null) { _highlightersRoot.Children.Add(hcVisual); } return hcVisual; } else { return _regularInkVisuals; } } #endregion #region Fields // The renderer's top level container visuals private ContainerVisual _rootVisual; private ContainerVisual _highlightersRoot; private ContainerVisual _incrementalRenderingVisuals; private ContainerVisual _regularInkVisuals; // Stroke-to-visual map private Dictionary<Stroke, StrokeVisual> _visuals; // Color-to-visual map for highlighter ink container visuals private Dictionary<Color, HighlighterContainerVisual> _highlighters = null; // Collection of strokes this Renderer renders private StrokeCollection _strokes = null; // List of visuals attached via AttachIncrementalRendering private List<Visual> _attachedVisuals = null; // When true, will render in high contrast mode private bool _highContrast; private Color _highContrastColor = Colors.White; #endregion } }