// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // // Description: This is the partial class of the StickyNoteControl. // This file contains all CAF annotation related implementations. // // See spec at StickyNoteControlSpec.mht // using MS.Internal; using MS.Internal.Annotations; using MS.Internal.Annotations.Component; using MS.Internal.Controls.StickyNote; using System.Collections; using System.Globalization; using System.Windows; using System.Windows.Annotations; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Threading; using System.Xml; using MS.Internal.Documents; using System.Windows.Controls.Primitives; // IScrollInfo using MS.Utility; namespace MS.Internal.Controls.StickyNote { // NOTICE-2004/08/18, // Whenever we add a new type data to this enum type, make sure to update SNCAnnotation.AllValues and SNCAnnotation.AllContents. // This is a collection which contains all the Xml elements being used by StickyNoteControl CAF schema. [System.Flags] internal enum XmlToken { MetaData = 0x00000001, Left = 0x00000004, Top = 0x00000008, XOffset = 0x00000010, YOffset = 0x00000020, Width = 0x00000080, Height = 0x00000100, IsExpanded = 0x00000200, Author = 0x00000400, Text = 0x00002000, Ink = 0x00008000, ZOrder = 0x00020000 } // This a wrapper class which encapsulates the operations for dealing with CAF's cargo, resources and XmlElements. // The schema can be found in http://tabletpc/longhorn/Specs/WinFX%20StickyNoteControl%20M8.1.mht#_Toc79371211 internal class SNCAnnotation { //------------------------------------------------------------------------------- // // Constructors // //------------------------------------------------------------------------------- #region Constructor // This is a static constructor which initializes the internal xml name table and the name space manager. static SNCAnnotation() { // Create our xml name dictionary. s_xmlTokeFullNames = new Dictionary<XmlToken, string>(); // Fill in the name dictionary. foreach (XmlToken val in Enum.GetValues<XmlToken>()) { AddXmlTokenNames(val); } } public SNCAnnotation(Annotation annotation) { Debug.Assert(annotation != null); _annotation = annotation; _isNewAnnotation = _annotation.Cargos.Count == 0; // Initialize the data cache collection. _cachedXmlElements = new Dictionary<XmlToken, object>(); } private SNCAnnotation() { } #endregion Constructor //------------------------------------------------------------------------------- // // Public Methods // //------------------------------------------------------------------------------- #region Public Methods /// <summary> /// This static method will update an Annotation object with the specified data in a StickyNoteControl /// </summary> /// <param name="token">A flag which indicates the data needs to be updated. The flag can be combinated with the any valid bits.</param> /// <param name="snc">A StickyNoteControl instance</param> /// <param name="sncAnnotation">An SNCAnnotation object which contains a CAF annotation object</param> public static void UpdateAnnotation(XmlToken token, StickyNoteControl snc, SNCAnnotation sncAnnotation) { AnnotationService service = null; bool autoFlush = false; try { service = AnnotationService.GetService(((IAnnotationComponent)snc).AnnotatedElement); if (service != null && service.Store != null) { autoFlush = service.Store.AutoFlush; // Temporarily turn off autoflush until we are done // updating all the necessary values service.Store.AutoFlush = false; } Debug.Assert((token & AllValues) != 0); // Update Ink if ((token & XmlToken.Ink) != 0 && snc.Content.Type == StickyNoteType.Ink) { sncAnnotation.UpdateContent(snc, true, XmlToken.Ink); } // Update Text if ((token & XmlToken.Text) != 0 && snc.Content.Type == StickyNoteType.Text) { sncAnnotation.UpdateContent(snc, true, XmlToken.Text); } // Update MetaData if ((token & NegativeAllContents) != 0) { UpdateMetaData(token, snc, sncAnnotation); } } finally { if (service != null && service.Store != null) { // If auto flush was true before, setting it to true again should cause a flush. service.Store.AutoFlush = autoFlush; } } } /// <summary> /// This static method will update a StickyNoteControl object with the specified data in an Annotation /// </summary> /// <param name="token">A flag which indicates the data needs to be updated. The flag can be combinated with the any valid bits.</param> /// <param name="snc">A StickyNoteControl instance</param> /// <param name="sncAnnotation">An SNCAnnotation object which contains a CAF annotation object</param> public static void UpdateStickyNoteControl(XmlToken token, StickyNoteControl snc, SNCAnnotation sncAnnotation) { Invariant.Assert((token & AllValues) != 0, "No token specified."); Invariant.Assert(snc != null, "Sticky Note Control is null."); Invariant.Assert(sncAnnotation != null, "Annotation is null."); // FUTURE-2004/08/18-WAYNEZEN, // Updating the xml data is synchronized below which could pontentially block the UIContext // for updating the huge amount data like Ink. // We could conside to post the callbacks to the UIContext for update data in the future. XmlAttribute node; // Update Ink if ((token & XmlToken.Ink) != 0 && sncAnnotation.HasInkData) { sncAnnotation.UpdateContent(snc, false, XmlToken.Ink); } // Update Text if ((token & XmlToken.Text) != 0 && sncAnnotation.HasTextData) { sncAnnotation.UpdateContent(snc, false, XmlToken.Text); } // Update Author if ((token & XmlToken.Author) != 0) { int nCount = sncAnnotation._annotation.Authors.Count; // Get the culture specific text separator. string listSeparator = snc.Language.GetSpecificCulture().TextInfo.ListSeparator; string authors = string.Empty; for (int i = 0; i < nCount; i++) { if (i != 0) { authors += listSeparator + sncAnnotation._annotation.Authors[i]; } else { authors += sncAnnotation._annotation.Authors[i]; } } // Setting the author property will cause the UI to update snc.SetValue(StickyNoteControl.AuthorPropertyKey, authors); } // Update Height if ((token & XmlToken.Height) != 0) { node = (XmlAttribute)sncAnnotation.FindData(XmlToken.Height); if (node != null) { double height = Convert.ToDouble(node.Value, CultureInfo.InvariantCulture); snc.SetValue(FrameworkElement.HeightProperty, height); } else { snc.ClearValue(FrameworkElement.HeightProperty); } } // Update Width if ((token & XmlToken.Width) != 0) { node = (XmlAttribute)sncAnnotation.FindData(XmlToken.Width); if (node != null) { double width = Convert.ToDouble(node.Value, CultureInfo.InvariantCulture); snc.SetValue(FrameworkElement.WidthProperty, width); } else { snc.ClearValue(FrameworkElement.WidthProperty); } } // Update IsExpanded if ((token & XmlToken.IsExpanded) != 0) { node = (XmlAttribute)sncAnnotation.FindData(XmlToken.IsExpanded); if (node != null) { bool expanded = Convert.ToBoolean(node.Value, CultureInfo.InvariantCulture); snc.IsExpanded = expanded; } else { snc.ClearValue(StickyNoteControl.IsExpandedProperty); } } // Update ZOrder if ((token & XmlToken.ZOrder) != 0) { node = (XmlAttribute)sncAnnotation.FindData(XmlToken.ZOrder); if (node != null) { ((IAnnotationComponent)snc).ZOrder = Convert.ToInt32(node.Value, CultureInfo.InvariantCulture); } } // Update Position if ((token & PositionValues) != 0) { TranslateTransform transform = new TranslateTransform(); if ((token & XmlToken.Left) != 0) { node = (XmlAttribute)sncAnnotation.FindData(XmlToken.Left); if (node != null) { double left = Convert.ToDouble(node.Value, CultureInfo.InvariantCulture); // All 'left' values are persisted assuming two things: // 1) the top-left corner (visually) of the StickyNote is the origin of its coordinate space // 2) the positive x-axis of its parent is to the right // This flag signals that we have a positive x-axis to the left and our // top-right corner (visually) is our origin. So we need to flip the // value before using it. if (snc.FlipBothOrigins) { left = -(left + snc.Width); } transform.X = left; } } if ((token & XmlToken.Top) != 0) { node = (XmlAttribute)sncAnnotation.FindData(XmlToken.Top); if (node != null) { double top = Convert.ToDouble(node.Value, CultureInfo.InvariantCulture); transform.Y = top; } } // Now, we update the StickyNote offset if ((token & XmlToken.XOffset) != 0) { node = (XmlAttribute)sncAnnotation.FindData(XmlToken.XOffset); if (node != null) { snc.XOffset = Convert.ToDouble(node.Value, CultureInfo.InvariantCulture); } } if ((token & XmlToken.YOffset) != 0) { node = (XmlAttribute)sncAnnotation.FindData(XmlToken.YOffset); if (node != null) { snc.YOffset = Convert.ToDouble(node.Value, CultureInfo.InvariantCulture); } } // Set the adorner layer transform. snc.PositionTransform = transform; } } #endregion Public Methods //------------------------------------------------------------------------------- // // Public Fields // //------------------------------------------------------------------------------- #region Public Fields // A const field which contains all the Xml Elements which are mapped to a StickyNoteControl's properties. public const XmlToken AllValues = XmlToken.Left | XmlToken.Top | XmlToken.XOffset | XmlToken.YOffset | XmlToken.Width | XmlToken.Height | XmlToken.IsExpanded | XmlToken.Author | XmlToken.Text | XmlToken.Ink | XmlToken.ZOrder; // A const field which contains all the Xml Elements which are related to the StickyNotes persisted position. public const XmlToken PositionValues = XmlToken.Left | XmlToken.Top | XmlToken.XOffset | XmlToken.YOffset; // A const field which contains all the Xml Elements for initial draw of the StickyNote (this includes the offsets but not the ZOrder). public const XmlToken Sizes = XmlToken.Left | XmlToken.Top | XmlToken.XOffset | XmlToken.YOffset | XmlToken.Width | XmlToken.Height; // A const field which contains all the Xml Elements which are related to the properties of the its contents. public const XmlToken AllContents = XmlToken.Text | XmlToken.Ink; public const XmlToken NegativeAllContents = AllValues ^ XmlToken.Text ^ XmlToken.Ink; #endregion Public Fields //------------------------------------------------------------------------------- // // Public Properties // //------------------------------------------------------------------------------- #region Public Properties /// <summary> /// Returns whether this annotation is considered new by the StickyNoteControl. /// </summary> public bool IsNewAnnotation { get { return _isNewAnnotation; } } /// <summary> /// Returns whether this annotation has ink data. /// </summary> public bool HasInkData { get { return FindData(XmlToken.Ink) != null; } } /// <summary> /// Returns wether this annotation has text data. /// </summary> public bool HasTextData { get { return FindData(XmlToken.Text) != null; } } #endregion Public Properties //------------------------------------------------------------------------------- // // Private Methods // //------------------------------------------------------------------------------- #region Private Methods /// <summary> /// This method will find a specified cargo. /// </summary> /// <param name="cargoName">A specified cargo name</param> /// <returns>The existing cargo or null</returns> private AnnotationResource FindCargo(string cargoName) { foreach (AnnotationResource cargo in _annotation.Cargos) { if (cargoName.Equals(cargo.Name)) return cargo; } return null; } /// <summary> /// Find the specified Annotation data /// </summary> /// <param name="token">A flag which is corresponding to the annotation data</param> /// <returns>The annotation data or null</returns> private object FindData(XmlToken token) { // Assume that we can't find any thing. object ret = null; // First, check if we have the data cached. if (_cachedXmlElements.ContainsKey(token)) { ret = _cachedXmlElements[token]; } else { // Then, we try to search the data in the current annotation. AnnotationResource cargo = FindCargo(GetCargoName(token)); if (cargo != null) { ret = SNCAnnotation.FindContent(token, cargo); // If we found the data in annotation, we go ahead cache it. if (ret != null) { _cachedXmlElements.Add(token, ret); } } } return ret; } /// <summary> /// Returns the AnnotationResource and the XML root for the given token from the passed in annotation. /// If the cargo or root do not exist they are created but not added to the annotation. The newCargo /// and newRoot flags specify whether they were created. The caller must use these to add the items /// to the annotation after they are done with their modifications. /// </summary> /// <param name="annotation">the current annotation</param> /// <param name="token">the token to be processed</param> /// <param name="cargo">the cargo for the token</param> /// <param name="root">the root XML element</param> /// <param name="newCargo">means a new root and new cargo was created. the root was already added to the cargo but the cargo was not added to the annotation</param> /// <param name="newRoot">means a new root was created. it has not been added to the cargo.</param> private static void GetCargoAndRoot( SNCAnnotation annotation, XmlToken token, out AnnotationResource cargo, out XmlElement root, out bool newCargo, out bool newRoot) { Invariant.Assert(annotation != null, "Annotation is null."); Invariant.Assert((token & (AllValues | XmlToken.MetaData)) != 0, "No token specified."); string cargoName = GetCargoName(token); newRoot = false; newCargo = false; cargo = annotation.FindCargo(cargoName); // Cargo exists if (cargo != null) { root = FindRootXmlElement(token, cargo); // Uncommon situation - cargo created without root XmlElement if (root == null) { newRoot = true; XmlDocument xmlDoc = new XmlDocument(); root = xmlDoc.CreateElement(GetXmlName(token), AnnotationXmlConstants.Namespaces.BaseSchemaNamespace); // Don't add it to the cargo yet - wait until all the // values are set on it } } else { newCargo = true; cargo = new AnnotationResource(cargoName); XmlDocument xmlDoc = new XmlDocument(); root = xmlDoc.CreateElement(GetXmlName(token), AnnotationXmlConstants.Namespaces.BaseSchemaNamespace); // Since the cargo is new, its safe to add the root to it // No events will make it to the annotation yet cargo.Contents.Add(root); } } /// <summary> /// Updates the value of the specified token on the specified XmlElement. /// If the value is the same, no update is made. If the new value is null /// the attribute is removed. /// </summary> private void UpdateAttribute(XmlElement root, XmlToken token, string value) { string name = GetXmlName(token); XmlNode oldValue = root.GetAttributeNode(name, AnnotationXmlConstants.Namespaces.BaseSchemaNamespace); if (oldValue == null) { if (value == null) return; else root.SetAttribute(name, AnnotationXmlConstants.Namespaces.BaseSchemaNamespace, value); } else { if (value == null) root.RemoveAttribute(name, AnnotationXmlConstants.Namespaces.BaseSchemaNamespace); else if (oldValue.Value != value) root.SetAttribute(name, AnnotationXmlConstants.Namespaces.BaseSchemaNamespace, value); } } /// <summary> /// This static method returns the Xml name for the specified token. /// </summary> /// <param name="token">A specified token</param> /// <returns>The name for the Xml node</returns> private static string GetXmlName(XmlToken token) { return s_xmlTokeFullNames[token]; } /// <summary> /// This method is called by the static constructor to set up the Xml name dictionary. /// </summary> /// <param name="token">A specified token</param> private static void AddXmlTokenNames(XmlToken token) { // Conver the enum value to the string first. string xmlName = token.ToString(); // Depending on the data, we add the proper prefix to the full name. switch (token) { // The element names should be qualified case XmlToken.MetaData: case XmlToken.Text: case XmlToken.Ink: { s_xmlTokeFullNames.Add(token, $"{AnnotationXmlConstants.Prefixes.BaseSchemaPrefix}:{xmlName}"); } break; //the attribute names should be local case XmlToken.Left: case XmlToken.Top: case XmlToken.XOffset: case XmlToken.YOffset: case XmlToken.Width: case XmlToken.Height: case XmlToken.IsExpanded: case XmlToken.ZOrder: default: { s_xmlTokeFullNames.Add(token, xmlName); } break; } } /// <summary> /// Return the cargo name which hosts the specified data. /// </summary> /// <param name="token">The specified data</param> /// <returns>The host cargo name</returns> private static string GetCargoName(XmlToken token) { string cargoName; switch (token) { // Those tokens use *snc* prefix. case XmlToken.MetaData: case XmlToken.Left: case XmlToken.Top: case XmlToken.XOffset: case XmlToken.YOffset: case XmlToken.Width: case XmlToken.Height: case XmlToken.IsExpanded: case XmlToken.ZOrder: { cargoName = SNBConstants.MetaResourceName; } break; // Those tokens use *media* prefix. case XmlToken.Text: { cargoName = SNBConstants.TextResourceName; } break; case XmlToken.Ink: { cargoName = SNBConstants.InkResourceName; } break; default: { cargoName = string.Empty; Debug.Assert(false); } break; } return cargoName; } /// <summary> /// The method returns the root node which contains the specified data in a cargo. /// </summary> /// <param name="token">The specified data</param> /// <param name="cargo">The specified cargo</param> /// <returns>The root node or null</returns> private static XmlElement FindRootXmlElement(XmlToken token, AnnotationResource cargo) { Debug.Assert(cargo != null); XmlElement element = null; string xmlName = string.Empty; // Get the xml name of the root node switch (token) { case XmlToken.Text: case XmlToken.Ink: xmlName = GetXmlName(token); break; case XmlToken.MetaData: case XmlToken.IsExpanded: case XmlToken.Width: case XmlToken.Height: case XmlToken.Top: case XmlToken.Left: case XmlToken.XOffset: case XmlToken.YOffset: case XmlToken.ZOrder: xmlName = GetXmlName(XmlToken.MetaData); break; default: Debug.Assert(false); break; } // Search the root in the cargo's contents. foreach (XmlElement node in cargo.Contents) { if (node.Name.Equals(xmlName)) { element = node; break; } } return element; } /// <summary> /// Find the specified data in a cargo. /// </summary> /// <param name="token">The specified data</param> /// <param name="cargo">The cargo which we are searhing in</param> /// <returns>The data object or null</returns> private static object FindContent(XmlToken token, AnnotationResource cargo) { object content = null; XmlElement root = SNCAnnotation.FindRootXmlElement(token, cargo); // If we found the root node, we should use XPath to query the node which contains the corresponding data. // The StickyNoteControl's xml schema can be found // in http://tabletpc/longhorn/Specs/WinFX%20StickyNoteControl%20M8.1.mht#_Toc79371211 if (root != null) { switch (token) { case XmlToken.Text: case XmlToken.Ink: return root; case XmlToken.IsExpanded: case XmlToken.ZOrder: case XmlToken.Top: case XmlToken.Left: case XmlToken.XOffset: case XmlToken.YOffset: case XmlToken.Width: case XmlToken.Height: return root.GetAttributeNode(GetXmlName(token), AnnotationXmlConstants.Namespaces.BaseSchemaNamespace); default: Debug.Assert(false); break; } } return content; } // Update ink data from/to SNC. private void UpdateContent(StickyNoteControl snc, bool updateAnnotation, XmlToken token) { Invariant.Assert(snc != null, "Sticky Note Control is null."); Invariant.Assert((token & AllContents) != 0, "No token specified."); StickyNoteContentControl contentControl = snc.Content; // Template hasn't been applied yet. Once it has the content control will then be setup. if (contentControl == null) { return; } // Check whether the annotation data matches the content control. if ((token == XmlToken.Ink && contentControl.Type != StickyNoteType.Ink) || (token == XmlToken.Text && contentControl.Type != StickyNoteType.Text)) { Debug.Fail("The annotation data does match with the current content control in StickyNote"); return; } XmlElement root = null; if (updateAnnotation) { // Update annotation from SNC AnnotationResource cargo = null; bool newRoot = false; bool newCargo = false; // Check if the text is empty. if (!contentControl.IsEmpty) { GetCargoAndRoot(this, token, out cargo, out root, out newCargo, out newRoot); contentControl.Save(root); } else { string cargoName = GetCargoName(token); cargo = FindCargo(cargoName); if (cargo != null) { _annotation.Cargos.Remove(cargo); _cachedXmlElements.Remove(token); } } if (newRoot) { Invariant.Assert(root != null, "XmlElement should have been created."); Invariant.Assert(cargo != null, "Cargo should have been retrieved."); cargo.Contents.Add(root); } if (newCargo) { Invariant.Assert(cargo != null, "Cargo should have been created."); _annotation.Cargos.Add(cargo); } } else { // Update SNC from annotation // Check if we have the text data in the xml store. XmlElement node = (XmlElement)FindData(token); if (node != null) { contentControl.Load(node); } else { if (!contentControl.IsEmpty) { contentControl.Clear(); } } } } /// <summary> /// Update the metadata tokens specified in token. /// </summary> private static void UpdateMetaData(XmlToken token, StickyNoteControl snc, SNCAnnotation sncAnnotation) { bool newCargo, newRoot; AnnotationResource cargo; XmlElement root; GetCargoAndRoot(sncAnnotation, XmlToken.MetaData, out cargo, out root, out newCargo, out newRoot); // Update Expanded if ((token & XmlToken.IsExpanded) != 0) { bool expanded = snc.IsExpanded; sncAnnotation.UpdateAttribute(root, XmlToken.IsExpanded, expanded.ToString(CultureInfo.InvariantCulture)); } // Update Height if ((token & XmlToken.Height) != 0) { Debug.Assert(snc.IsExpanded); double height = (double)snc.GetValue(FrameworkElement.HeightProperty); sncAnnotation.UpdateAttribute(root, XmlToken.Height, height.ToString(CultureInfo.InvariantCulture)); } // Update Width if ((token & XmlToken.Width) != 0) { Debug.Assert(snc.IsExpanded); double width = (double)snc.GetValue(FrameworkElement.WidthProperty); sncAnnotation.UpdateAttribute(root, XmlToken.Width, width.ToString(CultureInfo.InvariantCulture)); } // Update Left if ((token & XmlToken.Left) != 0) { double left = snc.PositionTransform.X; // All 'left' values are persisted assuming two things: // 1) the top-left corner (visually) of the StickyNote is the origin of its coordinate space // 2) the positive x-axis of its parent is to the right // This flag signals that we have a positive x-axis to the left and our // top-right corner (visually) is our origin. So we need to flip the // value before persisting it. if (snc.FlipBothOrigins) { left = -(left + snc.Width); } sncAnnotation.UpdateAttribute(root, XmlToken.Left, left.ToString(CultureInfo.InvariantCulture)); } // Update Top if ((token & XmlToken.Top) != 0) { sncAnnotation.UpdateAttribute(root, XmlToken.Top, snc.PositionTransform.Y.ToString(CultureInfo.InvariantCulture)); } // Update XOffset if ((token & XmlToken.XOffset) != 0) { sncAnnotation.UpdateAttribute(root, XmlToken.XOffset, snc.XOffset.ToString(CultureInfo.InvariantCulture)); } // Update YOffset if ((token & XmlToken.YOffset) != 0) { sncAnnotation.UpdateAttribute(root, XmlToken.YOffset, snc.YOffset.ToString(CultureInfo.InvariantCulture)); } // Update ZOrder if ((token & XmlToken.ZOrder) != 0) { sncAnnotation.UpdateAttribute(root, XmlToken.ZOrder, ((IAnnotationComponent)snc).ZOrder.ToString(CultureInfo.InvariantCulture)); } if (newRoot) { cargo.Contents.Add(root); } if (newCargo) { sncAnnotation._annotation.Cargos.Add(cargo); } } #endregion Private Methods //------------------------------------------------------------------------------- // // Private Fields // //------------------------------------------------------------------------------- #region Private Fields private static Dictionary<XmlToken, string> s_xmlTokeFullNames; // A dictionary for the names of the xml elements private Dictionary<XmlToken, object> _cachedXmlElements; // A dictionary for caching the data object private Annotation _annotation; private readonly bool _isNewAnnotation; #endregion Private Fields } } namespace System.Windows.Controls { public partial class StickyNoteControl { //------------------------------------------------------------------------------- // // IAnnotationComponent Interface // //------------------------------------------------------------------------------- #region IAnnotationComponent Members /// <summary> /// Adds an attached annotations to this StickyNoteControl /// </summary> /// <param name="attachedAnnotation">An IAttachedAnnotation instance</param> void IAnnotationComponent.AddAttachedAnnotation(IAttachedAnnotation attachedAnnotation) { ArgumentNullException.ThrowIfNull(attachedAnnotation); if (_attachedAnnotation == null) { //fire trace event EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordAnnotation, EventTrace.Event.AddAttachedSNBegin); SetAnnotation(attachedAnnotation); //fire trace event EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordAnnotation, EventTrace.Event.AddAttachedSNEnd); } else { throw new InvalidOperationException(SR.AddAnnotationsNotImplemented); } } /// <summary> /// Removes an attached annotations from this StickyNoteControl. /// </summary> /// <param name="attachedAnnotation">An IAttachedAnnotation instance</param> void IAnnotationComponent.RemoveAttachedAnnotation(IAttachedAnnotation attachedAnnotation) { ArgumentNullException.ThrowIfNull(attachedAnnotation); if (attachedAnnotation == _attachedAnnotation) { //fire trace event EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordAnnotation, EventTrace.Event.RemoveAttachedSNBegin); GiveUpFocus(); ClearAnnotation(); //fire trace event EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordAnnotation, EventTrace.Event.RemoveAttachedSNEnd); } else { throw new ArgumentException(SR.InvalidValueSpecified, nameof(attachedAnnotation)); } } /// <summary> /// Called when an AttachedAnnotation's attached anchor changes. /// </summary> /// <param name="attachedAnnotation">The attached annotation after modification</param> /// <param name="previousAttachedAnchor">The attached anchor previously associated with the attached annotation.</param> /// <param name="previousAttachmentLevel">The previous attachment level of the attached annotation.</param> void IAnnotationComponent.ModifyAttachedAnnotation(IAttachedAnnotation attachedAnnotation, object previousAttachedAnchor, AttachmentLevel previousAttachmentLevel) { throw new NotSupportedException(SR.NotSupported); } /// <summary> /// Gets the attached annotations this component is representing. /// </summary> /// <returns>list of IAttachedAnnotation instances this component is representing</returns> IList IAnnotationComponent.AttachedAnnotations { get { ArrayList annotations = new ArrayList(1); if (_attachedAnnotation != null) { annotations.Add(_attachedAnnotation); } return annotations; } } /// <summary> /// Compute the transform for the icon. /// also hide or show the expanded component. /// </summary> /// <param name="transform"></param> /// <returns></returns> GeneralTransform IAnnotationComponent.GetDesiredTransform(GeneralTransform transform) { if (_attachedAnnotation != null) { // If we are expanded and set to flow from RightToLeft and we are in a viewer using a DocumentPageHost // we need to mirror ourselves. This is work around an issue with DocumentViewerBase which mirrors its // contents (because its generated always as LeftToRight). We are anchored to that content so the mirror // gets applied to as as well. This is our attempt to cancel out that mirror. if (this.IsExpanded && this.FlowDirection == FlowDirection.RightToLeft && _attachedAnnotation.Parent is DocumentPageHost) { _selfMirroring = true; } else { _selfMirroring = false; } Point anchor = _attachedAnnotation.AnchorPoint; if (double.IsInfinity(anchor.X) || double.IsInfinity(anchor.Y)) { throw new InvalidOperationException(SR.InvalidAnchorPosition); } if ((double.IsNaN(anchor.X)) || (double.IsNaN(anchor.Y))) return null; GeneralTransformGroup transformations = new GeneralTransformGroup(); // We should be in normal Right-To-Left mode, but because of special cases // in DocumentPageView, we've been mirrored. We detect this and re-mirror // ourselves. We also need to do special handling of move/resize operations. if (_selfMirroring) { // This is the mirroring transform that will get the StickyNote to lay out // as if it were in Right-To-Left mode transformations.Children.Add(new MatrixTransform(-1.0, 0.0, 0.0, 1.0, this.Width, 0.0)); } transformations.Children.Add(new TranslateTransform(anchor.X, anchor.Y)); TranslateTransform offsetTransform = new TranslateTransform(0, 0); if (IsExpanded) { offsetTransform = PositionTransform.Clone(); // Reset delta values _deltaX = _deltaY = 0; //if we are in any kind of page viewer we might need to bring SN on the page Rect rectPage = PageBounds; Rect rectStickyNote = StickyNoteBounds; // Get the current offsets double offsetX, offsetY; GetOffsets(rectPage, rectStickyNote, out offsetX, out offsetY); // If the current offsets are greater than the cached the values, // we will make sure that stickynote sticks on the cached values. if (DoubleUtil.GreaterThan(Math.Abs(offsetX), Math.Abs(_offsetX))) { // Whatever the offset - don't move to the right more than double offset = _offsetX - offsetX; if (DoubleUtil.LessThan(offset, 0)) // if we are moving to the left, don't go beyond the edge { offset = Math.Max(offset, -(rectStickyNote.Left - rectPage.Left)); } offsetTransform.X += offset; _deltaX = offset; } if (DoubleUtil.GreaterThan(Math.Abs(offsetY), Math.Abs(_offsetY))) { double offset = _offsetY - offsetY; if (DoubleUtil.LessThan(offset, 0)) // if we are moving to the top, don't go beyond the edge { offset = Math.Max(offset, -(rectStickyNote.Top - rectPage.Top)); } offsetTransform.Y += offset; _deltaY = offset; } } if (offsetTransform != null) transformations.Children.Add(offsetTransform); if (transform != null) transformations.Children.Add(transform); return transformations; } return null; } /// <summary> /// Return the attached annotation parent as the annotated element /// </summary> /// <value></value> UIElement IAnnotationComponent.AnnotatedElement { get { return _attachedAnnotation != null ? _attachedAnnotation.Parent as UIElement : null; } } /// <summary> /// Get/Set the PresentationContext /// </summary> /// <value></value> PresentationContext IAnnotationComponent.PresentationContext { get { return _presentationContext; } set { _presentationContext = value; } } /// <summary> /// Sets and gets the Z-order of this component. NOP - /// Highlight does not have Z-order /// </summary> /// <value>Context this annotation component is hosted in</value> int IAnnotationComponent.ZOrder { get { return _zOrder; } set { _zOrder = value; UpdateAnnotationWithSNC(XmlToken.ZOrder); } } /// <summary> /// Notifies the component that the AnnotatedElement content has changed /// </summary> bool IAnnotationComponent.IsDirty { get { if (_anchor != null) return _anchor.IsDirty; return false; } set { _anchor?.IsDirty = value; if (value) InvalidateVisual(); } } #endregion // IAnnotationComponent Members #region Public Fields /// <summary> /// The Xml type name which is used by the Annotation to instantiate a Text StickyNoteControl /// </summary> public static readonly XmlQualifiedName TextSchemaName = new XmlQualifiedName("TextStickyNote", AnnotationXmlConstants.Namespaces.BaseSchemaNamespace); /// <summary> /// The Xml type name which is used by the Annotation to instantiate an Ink StickyNoteControl /// </summary> public static readonly XmlQualifiedName InkSchemaName = new XmlQualifiedName("InkStickyNote", AnnotationXmlConstants.Namespaces.BaseSchemaNamespace); #endregion Public Fields //------------------------------------------------------------------------------- // // Internal Properties // //------------------------------------------------------------------------------- #region Internal Properties // The property is the accessor of the variable of _positionTransform which is used by // IAnnotationComponent.GetDesiredTransform method. The annotation adorner layer will use the transform to // position the StickyNoteControl internal TranslateTransform PositionTransform { get { return _positionTransform; } set { Invariant.Assert(value != null, "PositionTransform cannot be null."); _positionTransform = value; InvalidateTransform(); } } /// <summary> /// Gets/Sets the cached X offset when StikcyNote is cross the page boundary /// </summary> internal double XOffset { get { return _offsetX; } set { _offsetX = value; } } /// <summary> /// Gets/Sets the cached Y offset when StikcyNote is cross the page boundary /// </summary> internal double YOffset { get { return _offsetY; } set { _offsetY = value; } } /// <summary> /// This flag signals that we are truly in Right-To-Left mode. This means the positive /// x-axis points to the left and our top-right corner (visually) is our origin. We use /// this flag to determine whether we should flip the value for 'left' before storing it. /// Flipping it allows it to be persisted in a way that can be used by the StickyNote in /// non Right-To-Left mode or in the self-mirroring case. /// </summary> internal bool FlipBothOrigins { get { return (this.IsExpanded && this.FlowDirection == FlowDirection.RightToLeft && _attachedAnnotation != null && _attachedAnnotation.Parent is DocumentPageHost); } } #endregion Internal Properties //------------------------------------------------------------------------------- // // Private Methods // //------------------------------------------------------------------------------- #region Private Methods // A registered handler for a bubble listening to the annotation author update event. // obj - The sender of the event // args - event arguement private void OnAuthorUpdated(object obj, AnnotationAuthorChangedEventArgs args) { Debug.Assert(_attachedAnnotation != null && _attachedAnnotation.Annotation == args.Annotation); if (!InternalLocker.IsLocked(LockHelper.LockFlag.AnnotationChanged)) { UpdateSNCWithAnnotation(XmlToken.Author); IsDirty = true; } } // A registered handler for a bubble listening to the annotation store update event. // obj - The sender of the event // args - event arguement private void OnAnnotationUpdated(object obj, AnnotationResourceChangedEventArgs args) { Debug.Assert(_attachedAnnotation != null && _attachedAnnotation.Annotation == args.Annotation); if (!InternalLocker.IsLocked(LockHelper.LockFlag.AnnotationChanged)) { SNCAnnotation sncAnnotation = new SNCAnnotation(args.Annotation); _sncAnnotation = sncAnnotation; UpdateSNCWithAnnotation(SNCAnnotation.AllValues); IsDirty = true; } } /// <summary> /// The method sets an instance of the IAttachedAnnotation to the StickyNoteControl. /// It will be called by IAnnotationComponent.AddAttachedAnnotation. /// </summary> /// <param name="attachedAnnotation">The instance of the IAttachedAnnotation</param> private void SetAnnotation(IAttachedAnnotation attachedAnnotation) { SNCAnnotation sncAnnotation = new SNCAnnotation(attachedAnnotation.Annotation); // Retrieve the data type. Then set the StickyNote to correct type. // If we have empty data, we won't change the current StickyNote type. bool hasInkData = sncAnnotation.HasInkData; bool hasTextData = sncAnnotation.HasTextData; if (hasInkData && hasTextData) { throw new ArgumentException(SR.InvalidStickyNoteAnnotation, nameof(attachedAnnotation)); } else if (hasInkData) { _stickyNoteType = StickyNoteType.Ink; } else if (hasTextData) { _stickyNoteType = StickyNoteType.Text; } // If we already created a Content control, make sure it matches our new type or // gets recreated to match. if (Content != null) { EnsureStickyNoteType(); } //create cargo if it is a new Annotation so it is not considered as new next time if (sncAnnotation.IsNewAnnotation) { AnnotationResource cargo = new AnnotationResource(SNBConstants.MetaResourceName); attachedAnnotation.Annotation.Cargos.Add(cargo); } // Set the internal variables _attachedAnnotation = attachedAnnotation; _attachedAnnotation.Annotation.CargoChanged += new AnnotationResourceChangedEventHandler(OnAnnotationUpdated); _attachedAnnotation.Annotation.AuthorChanged += new AnnotationAuthorChangedEventHandler(OnAuthorUpdated); _sncAnnotation = sncAnnotation; _anchor.AddAttachedAnnotation(attachedAnnotation); // Update all value UpdateSNCWithAnnotation(SNCAnnotation.AllValues); // The internal data is just sync'ed to the store. So, reset the dirty to false. IsDirty = false; //now check if the SN must be seen if ((_attachedAnnotation.AttachmentLevel & AttachmentLevel.StartPortion) == 0) { //we do not need to show the StickyNote SetValue(UIElement.VisibilityProperty, Visibility.Collapsed); } else { //if it is seen we need to take care about bringing into view when needed RequestBringIntoView += new RequestBringIntoViewEventHandler(OnRequestBringIntoView); } } /// <summary> /// Clear the internal variables and events which are related to the annotation. /// The method will be called by IAnnotationComponent.RemoveAttachedAnnotation /// </summary> private void ClearAnnotation() { _attachedAnnotation.Annotation.CargoChanged -= new AnnotationResourceChangedEventHandler(OnAnnotationUpdated); _attachedAnnotation.Annotation.AuthorChanged -= new AnnotationAuthorChangedEventHandler(OnAuthorUpdated); _anchor.RemoveAttachedAnnotation(_attachedAnnotation); _sncAnnotation = null; _attachedAnnotation = null; RequestBringIntoView -= new RequestBringIntoViewEventHandler(OnRequestBringIntoView); } /// <summary> /// Brings the SN into view. When navigate through the keyboard navigation the focus /// can get to a SN that is not currently into view. /// </summary> /// <param name="sender">sender</param> /// <param name="e">arguments</param> /// <remarks> Since the AdornerLayer does not have scrolling abilities we need to scroll /// AnnotatedElement. In order to get out SN into view we must calculate the corresponding area of /// the AnnotatedElement.</remarks> private void OnRequestBringIntoView(Object sender, RequestBringIntoViewEventArgs e) { Debug.Assert(((IAnnotationComponent)this).AnnotatedElement != null, "undefined annotated element"); FrameworkElement target = ((IAnnotationComponent)this).AnnotatedElement as FrameworkElement; DocumentPageHost host = target as DocumentPageHost; if (host != null) { target = host.PageVisual as FrameworkElement; } if (target == null) { //we have nothing to do here return; } //if target is IScrollInfo - check if we are within the viewport IScrollInfo scrollInfo = target as IScrollInfo; if (scrollInfo != null) { Rect bounds = StickyNoteBounds; Rect viewport = new Rect(0, 0, scrollInfo.ViewportWidth, scrollInfo.ViewportHeight); if (bounds.IntersectsWith(viewport)) return; } //get adorned element Transform adornerTransform = (Transform)TransformToVisual(target); Debug.Assert(adornerTransform != null, "transform to AnnotatedElement is null"); //get SN sizes Rect rect = new Rect(0, 0, Width, Height); rect.Transform(adornerTransform.Value); // Schedule BringIntoView, rather than making direct call. // Otherwise in some cases this can cause nested RequestBringIntoView call to be issued. // In scenario when document is inside scroll viewer and annotation needs to be // brought to view the call to BringIntoView won't work because of nested events. Dispatcher.BeginInvoke(DispatcherPriority.Normal, new DispatcherOperationCallback(DispatchBringIntoView), new object[] { target, rect }); } /// <summary> /// Schedules BringIntoView call on <see cref="IAnnotationComponent.AnnotatedElement"/> from <see cref="OnRequestBringIntoView"/> /// </summary> /// <param name="arg">object array of size 2. param[0] is target to bring into view, param[1] is target Rect</param> /// <returns>null</returns> private object DispatchBringIntoView(object arg) { object[] args = (object[])arg; FrameworkElement target = (FrameworkElement)(args[0]); Rect rect = (Rect)(args[1]); target.BringIntoView(rect); return null; } /// <summary> /// This method will update this StickyNoteControl data based on the internal annotation variable. /// </summary> /// <param name="tokens">The data need to be updated</param> private void UpdateSNCWithAnnotation(XmlToken tokens) { if (_sncAnnotation != null) { //fire trace event EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordAnnotation, EventTrace.Event.UpdateSNCWithAnnotationBegin); // Now, we are going to update this StickyNoteControl. We will get notified for the data being changed. // we don't want to update our internal annotation because for the data changes which are coming // from the annotation itself. So, we lock our internal locker. using (LockHelper.AutoLocker locker = new LockHelper.AutoLocker(InternalLocker, LockHelper.LockFlag.DataChanged)) { SNCAnnotation.UpdateStickyNoteControl(tokens, this, _sncAnnotation); } //fire trace event EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordAnnotation, EventTrace.Event.UpdateSNCWithAnnotationEnd); } } /// <summary> /// This method will update this internal annotation based on this StickyNoteControl. /// </summary> /// <param name="tokens"></param> private void UpdateAnnotationWithSNC(XmlToken tokens) { // Check if we have an annotation attached. // Also we don't want to update the annotation when the data changes are actually caused by UpdateSNCWithAnnotation. if (_sncAnnotation != null && !InternalLocker.IsLocked(LockHelper.LockFlag.DataChanged)) { //fire trace event EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordAnnotation, EventTrace.Event.UpdateAnnotationWithSNCBegin); // Now, we are going to update the annotation. Since we will get notified from the annotation store, // we don't want to update our internal annotation if the change has been made by this instance. // Here we lock the internal locker. using (LockHelper.AutoLocker locker = new LockHelper.AutoLocker(InternalLocker, LockHelper.LockFlag.AnnotationChanged)) { // Now, update the attached annotation. SNCAnnotation.UpdateAnnotation(tokens, this, _sncAnnotation); } //fire trace event EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordAnnotation, EventTrace.Event.UpdateAnnotationWithSNCEnd); } } /// <summary> /// The method will update the X and Y offsets when StickyNote is cross the host page boundary. /// This method should only be called from OnDragDelta. /// </summary> private void UpdateOffsets() { // If we don't have annotation, don't even borther. if (_attachedAnnotation != null) { Rect rectPage = PageBounds; Rect rectStickyNote = StickyNoteBounds; // Make sure that we have the valid bounds. if (!rectPage.IsEmpty && !rectStickyNote.IsEmpty) { // StickyNote should never disappear from the host page. Invariant.Assert(DoubleUtil.GreaterThan(rectStickyNote.Right, rectPage.Left), "Note's right is off left of page."); Invariant.Assert(DoubleUtil.LessThan(rectStickyNote.Left, rectPage.Right), "Note's left is off right of page."); Invariant.Assert(DoubleUtil.GreaterThan(rectStickyNote.Bottom, rectPage.Top), "Note's bottom is off top of page."); Invariant.Assert(DoubleUtil.LessThan(rectStickyNote.Top, rectPage.Bottom), "Note's top is off bottom of page."); double offsetX, offsetY; // Get the current offsets. GetOffsets(rectPage, rectStickyNote, out offsetX, out offsetY); // If the offsets have been changed, we should update the cached values. if (!DoubleUtil.AreClose(XOffset, offsetX)) { XOffset = offsetX; } if (!DoubleUtil.AreClose(YOffset, offsetY)) { YOffset = offsetY; } } } } /// <summary> /// A method which calculates the X and Y offsets between StickyNote and Page bounds. /// </summary> /// <param name="rectPage">The page bounds</param> /// <param name="rectStickyNote">The StickyNote bounds</param> /// <param name="offsetX"> /// X Offset. /// 0 means that StickyNote is completely inside page on X dimension. /// A negative value means that StickyNote is partially beyond page left. /// A positive value means that StickyNote is partially beyond page right. /// </param> /// <param name="offsetY"> /// Y Offset. /// 0 means that StickyNote is completely inside page on Y dimension. /// A negative value means that StickyNote is partially beyond page top. /// A positive value means that StickyNote is partially beyond page bottom. /// </param> private static void GetOffsets(Rect rectPage, Rect rectStickyNote, out double offsetX, out double offsetY) { offsetX = 0; if (DoubleUtil.LessThan(rectStickyNote.Left, rectPage.Left)) { // StickyNote is beyond the left boundary offsetX = rectStickyNote.Left - rectPage.Left; } else if (DoubleUtil.GreaterThan(rectStickyNote.Right, rectPage.Right)) { // StickyNote is beyond the right boundary offsetX = rectStickyNote.Right - rectPage.Right; } offsetY = 0; if (DoubleUtil.LessThan(rectStickyNote.Top, rectPage.Top)) { // StickyNote is beyond the top boundary offsetY = rectStickyNote.Top - rectPage.Top; } else if (DoubleUtil.GreaterThan(rectStickyNote.Bottom, rectPage.Bottom)) { // StickyNote is beyond the bottom boundary offsetY = rectStickyNote.Bottom - rectPage.Bottom; } } private Rect StickyNoteBounds { get { Debug.Assert(_attachedAnnotation != null, "This property should never be acccessed from outside of CAF"); Rect ret = Rect.Empty; Point anchor = _attachedAnnotation.AnchorPoint; if (!(double.IsNaN(anchor.X)) && !(double.IsNaN(anchor.Y)) && PositionTransform != null) { ret = new Rect(anchor.X + PositionTransform.X + _deltaX, anchor.Y + PositionTransform.Y + _deltaY, Width, Height); } return ret; } } private Rect PageBounds { get { Rect pageBounds = Rect.Empty; IAnnotationComponent component = (IAnnotationComponent)this; // If the annotated element is a scroll info, we should use the // full size of the scrollable content - ExtendWidth/ExtentHeight. IScrollInfo scrollInfo = component.AnnotatedElement as IScrollInfo; if (scrollInfo != null) { pageBounds = new Rect(-scrollInfo.HorizontalOffset, -scrollInfo.VerticalOffset, scrollInfo.ExtentWidth, scrollInfo.ExtentHeight); } else { UIElement parent = component.AnnotatedElement; if (parent != null) { Size pageSize = parent.RenderSize; pageBounds = new Rect(0, 0, pageSize.Width, pageSize.Height); } } return pageBounds; } } #endregion // Private Methods //------------------------------------------------------------------------------- // // Private Fields // //------------------------------------------------------------------------------- #region Private Fields /// <summary> /// the presentation context this sticky note is in /// </summary> private PresentationContext _presentationContext; /// <summary> /// Offset from anchor point to sticky note icon /// </summary> private TranslateTransform _positionTransform = new TranslateTransform(0, 0); // A reference of the current attached annotation instance. private IAttachedAnnotation _attachedAnnotation; private SNCAnnotation _sncAnnotation; // The cached horizontal and vertical portions of the StickyNote the user // put off the page boundary. This is used to attempt to reproduce the same // portions when the page has been reflowed. // 0 means that StickyNote is completely inside page on X or Y dimension. // A negative value means that StickyNote is partially beyond page top/left. // A positive value means that StickyNote is partially beyond page bottom/right. // These are updated everytime the user moves the StickyNote and are persisted. private double _offsetX; private double _offsetY; // The distances the StickyNote has been moved at layout time in order to // reproduce the same portion off the page as the user previously created. // 0 means we had to do no adjusting, the note is where the use put it. // A negative value means we had to move the StickyNote towards the origin. // A positive value menas we had to move the StickyNote away from the origin. // These are updated on every layout pass and are not persisted. private double _deltaX; private double _deltaY; //Component Z-order private int _zOrder; // This flag signals that we should be in normal Right-To-Left mode, but because // of special cases in DocumentPageView, we've been mirrored (back to Left-To-Right mode). // Therefore we need to re-mirror ourselves and special handling of move/resize operations. private bool _selfMirroring = false; #endregion // Private Fields } }