New Annotation type

Dec 31, 2010 at 11:10 AM

Hi Koen,

We're very excited to say that our client has accepted our proposal to create a full-blown document viewer based on your excellent Document Toolkit.  We have completed all the business logic and UI for the project, but we are having trouble creating an annotation type they would like to see and I was wondering if you could take a crake at it for us.

  • Draw rectangle over an area on the page (possible resize box?
  • Once the mouse is let go (mouse up event), the BoxNote annotation would be created in a similar fashion to the sticky note: allowing users to enter text, complete form fields if they exist, etc.
  • Move the annotation around on the page similar to the current sticky annotation.

I'm guessing this would be some kind of combination of the pen strokes annotation and sticky note annotation.

  • You would click a button on the toolbar (or context menu) to enable BoxNotes.
  • You would then draw the box notes you want to add.
  • Once finished, you would disable BoxNote annotations (again similar to pen stroke annotations).

We're just struggling with registering for the mouse events and drawing the shape.  We can complete the BoxNote factory so that it contains all the information we want to capture in the annotation, if we could only get it drawn properly!

The current Sticky Note always places it at the top of the screen, and is only one size.  So we would like to draw various type of boxes and add notes in a more streamlined fashion.

 

Let me know what you think.  I appreciate all your help!!

Thanks!

Eric

Coordinator
Jan 3, 2011 at 2:22 PM
Edited Jan 5, 2011 at 1:29 PM

Hi Eric,

It looks like you need a solution for rendering a rectangle on a page and then you should be able to re-use the sticky note annotation. Rendering a rectangle on a page is probably best implemented using a behavior. See below a sample implementation of such behavior.

 

    public class DrawRectangleBehavior
        : Behavior<DocumentViewer>
    {
        public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.Register("IsEnabled",
typeof(bool), typeof(DrawRectangleBehavior),
new PropertyMetadata(false, new PropertyChangedCallback(OnIsEnabledChanged)));
        private Popup popup = new Popup();
        private FixedPageViewer associatedFixedPageViewer;
        private Polygon shape;
        private Point startPosition;

        private static void OnIsEnabledChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            ((DrawRectangleBehavior)o).StopCurrentMouseOperation();
        }

        /// <summary>
        /// Gets or sets a value indicating whether this instance is enabled.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is enabled; otherwise, <c>false</c>.
        /// </value>
        public bool IsEnabled
        {
            get { return (bool)GetValue(IsEnabledProperty); }
            set { SetValue(IsEnabledProperty, value); }
        }

        /// <summary>
        /// Called after the behavior is attached to an AssociatedObject.
        /// </summary>
        /// <remarks>Override this to hook up functionality to the AssociatedObject.</remarks>
        protected override void OnAttached()
        {
            base.OnAttached();

            // add mouse down event handler that work even when already handled
            this.AssociatedObject.AddHandler(FrameworkElement.MouseLeftButtonDownEvent,
new MouseButtonEventHandler(OnMouseLeftButtonDown), true); this.AssociatedObject.MouseLeftButtonUp += OnMouseLeftButtonUp; this.AssociatedObject.MouseMove += OnMouseMove; this.AssociatedObject.LostMouseCapture += OnLostMouseCapture; } private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (!this.IsEnabled) { return; } this.associatedFixedPageViewer = GetFixedPageViewerFromMousePosition(e); if (associatedFixedPageViewer == null) { // no page selection, return return; } this.AssociatedObject.CaptureMouse(); this.startPosition = e.GetPosition(null); this.shape = new Polygon { Fill = new SolidColorBrush(Color.FromArgb(0x22, 0xff, 0, 0)), Stroke = new SolidColorBrush(Colors.Red), StrokeThickness = 1, }; this.shape.StrokeDashArray.Add(1); this.shape.StrokeDashArray.Add(2); this.shape.Points = CollectPoints(this.startPosition); this.popup.Child = this.shape; this.popup.IsOpen = true; } private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { var annotations = this.AssociatedObject.Annotations; // TODO: create annotation StopCurrentMouseOperation(); } private void OnLostMouseCapture(object sender, MouseEventArgs e) { StopCurrentMouseOperation(); } private void OnMouseMove(object sender, MouseEventArgs e) { if (this.shape != null) { this.shape.Points = CollectPoints(e.GetPosition(null)); } } private FixedPageViewer GetFixedPageViewerFromMousePosition(MouseButtonEventArgs e) { var element = (FrameworkElement)e.OriginalSource; // find the fixed page instance that has been selected return element.AncestorsAndSelf().OfType<FixedPageViewer>().FirstOrDefault(); } private PointCollection CollectPoints(Point endPosition) { // calculate DocumentViewer bounds relative to root visual var transform = this.associatedFixedPageViewer.TransformToVisual(Application.Current.RootVisual); var bounds = transform.TransformBounds(
new Rect(0, 0, this.associatedFixedPageViewer.ActualWidth, this.associatedFixedPageViewer.ActualHeight)); var points = new PointCollection(); var minX = Math.Max(bounds.Left, Math.Min(this.startPosition.X, endPosition.X)); var maxX = Math.Min(bounds.Right, Math.Max(this.startPosition.X, endPosition.X)); var minY = Math.Max(bounds.Top, Math.Min(this.startPosition.Y, endPosition.Y)); var maxY = Math.Min(bounds.Bottom, Math.Max(this.startPosition.Y, endPosition.Y)); // add .5 to prevent sub-pixel rendering points.Add(new Point(minX + .5, minY + .5)); points.Add(new Point(maxX + .5, minY + .5)); points.Add(new Point(maxX + .5, maxY + .5)); points.Add(new Point(minX + .5, maxY + .5)); return points; } private void StopCurrentMouseOperation() { if (this.shape != null) { this.AssociatedObject.ReleaseMouseCapture(); this.popup.IsOpen = false; this.shape = null; } } }

 


You'll need to associate this behavior with a DocumentViewer instance like so:

 

<doc:DocumentViewer >
  <i:Interaction.Behaviors>
    <test:MyBehavior IsEnabled="True" />
  </i:Interaction.Behaviors>
</doc:DocumentViewer>

 



The test prefix should refer to your namespace. Use the IsEnabled property to enable/disable the behavior based on a (toggle) button click.

Hope this helps,

- Koen

Jan 3, 2011 at 9:29 PM
That is a big help! I've got it all wired up, but the compiler is complaining about the following line: return element.AncestorsAndSelf().OfType().FirstOrDefault(); My guess is I don't have the correct DLL or "using" declaration. I thought it would be contained in the System.Xml.Linq library, but it doesn't have the method for .AncestorsAndSelf. Thoughts? Thanks again! Eric Eric Odom Datalytics LLC toll free 800.763.1957 ext.301 cellular 512.468.7847 eric@datalyticsllc.com Confidentiality Notice: This e-mail message, including any attachments are from Datalytics LLC, and are for the sole use of the intended recipient and may contain confidential and privileged information. Any unauthorized review, use, disclosure or distribution is prohibited. If you are not the intended recipient, please contact the sender by contacting Datalytics at 1-800-763-1957 and destroy all copies of the original message. On Jan 3, 2011, at 8:22 AM, kozw wrote: > return element.AncestorsAndSelf().OfType().FirstOrDefault(); That is a big help! I've got it all wired up, but the compiler is complaining about the following line:

return element.AncestorsAndSelf().OfType<FixedPageViewer>().FirstOrDefault();
My guess is I don't have the correct DLL or "using" declaration. I thought it would be contained in the System.Xml.Linq library, but it doesn't have the method for .AncestorsAndSelf.

Thoughts?

Thanks again!
Eric


Eric Odom
Datalytics LLC
toll free 800.763.1957 ext.301
cellular 512.468.7847


Confidentiality Notice: This e-mail message, including any attachments are from Datalytics LLC, and are for the sole use of the intended recipient and may contain confidential and privileged information. Any unauthorized review, use, disclosure or distribution is prohibited. If you are not the intended recipient, please contact the sender by contacting Datalytics at 1-800-763-1957 and destroy all copies of the original message.



On Jan 3, 2011, at 8:22 AM, kozw wrote:

return element.AncestorsAndSelf().OfType<FixedPageViewer>().FirstOrDefault();

Coordinator
Jan 3, 2011 at 9:35 PM
Edited Jan 3, 2011 at 9:36 PM

AncestorsAndSelf is an extension method implemented in the DependencyObjectExtensions class available in the FirstFloor.Documents assembly.

Just add a using FirstFloor.Documents.Controls statement and make sure the FirstFloor.Documents assembly is referenced.

 

- Koen

Jan 3, 2011 at 11:08 PM
Got it working. I was missing the System.Linq library. One more question: how do I get a reference to the Annotations Store from this Behavior class? The only method I see requires TextContainor. Thanks! Eric Eric Odom Datalytics LLC toll free 800.763.1957 ext.301 cellular 512.468.7847 eric@datalyticsllc.com Confidentiality Notice: This e-mail message, including any attachments are from Datalytics LLC, and are for the sole use of the intended recipient and may contain confidential and privileged information. Any unauthorized review, use, disclosure or distribution is prohibited. If you are not the intended recipient, please contact the sender by contacting Datalytics at 1-800-763-1957 and destroy all copies of the original message. On Jan 3, 2011, at 8:22 AM, kozw wrote: > > > Got it working. I was missing the System.Linq library. One more question: how do I get a reference to the Annotations Store from this Behavior class? The only method I see requires TextContainor.

Thanks!
Eric


Eric Odom
Datalytics LLC
toll free 800.763.1957 ext.301
cellular 512.468.7847


Confidentiality Notice: This e-mail message, including any attachments are from Datalytics LLC, and are for the sole use of the intended recipient and may contain confidential and privileged information. Any unauthorized review, use, disclosure or distribution is prohibited. If you are not the intended recipient, please contact the sender by contacting Datalytics at 1-800-763-1957 and destroy all copies of the original message.



On Jan 3, 2011, at 8:22 AM, kozw wrote:

<i:Interaction.Behaviors>
    <test:MyBehavior IsEnabled="True" />
  </i:Interaction.Behaviors>

Coordinator
Jan 3, 2011 at 11:30 PM

In the OnMouseLeftButtonUp method the annotation store is retrieved from the associated object (which is a Document Viewer that implements ITextContainer).

Jan 4, 2011 at 1:16 AM
Thanks, got it figured out. One more question: what's the best way to get the coordinates from the rectangle shape, so I can convert the rectangle to a new annotation. My guess is that it's related to the transform, but I can't seem to get close. If I extract the points from the shape to get the top, left, width, height...it seems to be off. Depending on the size of the browser, the amount it varies will change, that's why it seems that it's related to the transform.

So, is there an easy way to convert the rectangle that is drawn on the FixedPage to an annotation with the identical size of the rectangle? I have a custom annotation that is basically a rectangle with top, left, width and height as properties that I pass in when creating the annotation on the DrawRectangle mouse-up event...just need to figure out the coordinates.

Thanks again for all your help! The DrawRectangle code you sent over worked perfectly!



On Jan 3, 2011, at 5:30 PM, kozw wrote:

From: kozw

In the OnMouseLeftButtonUp method the annotation store is retrieved from the associated object (which is a Document Viewer that implements ITextContainer).


Jan 4, 2011 at 3:50 AM
Well, I figured out the coordination conversion, and now have the custom annotations rendering properly on the page with the correct location and size. Might not be the best way, but it works. I would like to do some simple with the cursor when I enable the DrawRectangle behavior, and change the cursor to an error rather than the test i-beam. I've set it in a couple of places, but it always seems to default back to the iBeam. Any ideas?



On Jan 3, 2011, at 5:30 PM, kozw wrote:

From: kozw

In the OnMouseLeftButtonUp method the annotation store is retrieved from the associated object (which is a Document Viewer that implements ITextContainer).


Coordinator
Jan 5, 2011 at 12:45 PM

Hi Eric,

You'll need to transform the rectangle bounds back to the fixedpage coordinates. Below snippet demonstrates this by drawing a blue rectangle inside a fixed page in the mouse up.

        private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            // continue if a valid shape has been drawn over a valid fixed page
            if (this.associatedFixedPageViewer != null && this.associatedFixedPageViewer.FixedPage != null &&
                this.shape != null && this.shape.Points.Count == 4) {

                var points = this.shape.Points;

                // transform bounds relative to fixed page
                var transform = Application.Current.RootVisual.TransformToVisual(this.associatedFixedPageViewer.FixedPage);
                var bounds = transform.TransformBounds(new Rect(points[0], points[2]));

                // for now render a blue rectangle in the page itself
                var rectangle = new Rectangle {
                    Width = bounds.Width,
                    Height = bounds.Height,
                    Fill = new SolidColorBrush(Color.FromArgb(0x22, 0, 0, 0xff))
                };

                Canvas.SetLeft(rectangle, bounds.Left);
                Canvas.SetTop(rectangle, bounds.Top);

                this.associatedFixedPageViewer.FixedPage.Children.Add(rectangle);
            }


            StopCurrentMouseOperation();
        }

Hope this helps,

- Koen

Coordinator
Jan 5, 2011 at 1:27 PM

As far as the mouse cursor is concerned; DocumentViewer manages the cursor for text selection and panning, you need to disable this by setting DocumentViewer.IsMouseEnabled to false when you are in the 'draw rectangle' mode. Once disabled you can set a custom cursor to the associated object in the OnMouseLeftButtonDown and you should reset it in the StopCurrentMouseOperation. Something like this;

 

        private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            ....
            
            this.AssociatedObject.Cursor = Cursors.Hand;
            this.AssociatedObject.CaptureMouse();

            ...
        }

        private void StopCurrentMouseOperation()
        {
            if (this.shape != null) {
                this.AssociatedObject.ReleaseMouseCapture();
                this.AssociatedObject.Cursor = null;        // reset cursor

                this.popup.IsOpen = false;
                this.shape = null;
            }
        }

 

Please note that setting the AssociatedObject.IsMouseEnabled in the OnMouseLeftButtonDown won't have an effect. You'll need to set the IsMouseEnabled before that (like you probably do when a user selected a context menu item or presses a toolbar button).

 

Hope this helps,

- Koen

Nov 20, 2011 at 12:34 AM

While waiting for the synchronized scrolling solution, I also need something similar to this.

It's probably simpler, because I don't need to track the cursor, I just need something like: DrawRectangle(Rectangle r, int page, Postition p)

 

Should I still try to use the FixedPageViewer, and if so, how do I get one for the particular page? Method described here gets it from the MouseButtonEventArgs object which I won't have.

 

These are probably rookie questions, but I am still new to the Document Toolkit (which is great, by the way :))

 

Thank you,

Luka

Coordinator
Nov 21, 2011 at 1:07 PM

Hi Luka,

What exactly do you want to achieve? Maybe you can use the annotation framework and create your own custom annotation for rendering rectangles on pages?

Kind regards,

- Koen

Nov 21, 2011 at 7:04 PM

Well, I just need to have a rectangle-type annotation that may later be expanded with additional functionality (maybe to be clickable, for example). I guess this approach with behavior is not applicable in my case, I just need to derive my custom annotation class...?

 

The closest example I found is the ink type annotation in document toolkit extensions, please tell if there is more existing code that would give me a good start.

 

Thanks,

Luka

Coordinator
Nov 22, 2011 at 11:44 AM
Edited Nov 22, 2011 at 11:46 AM

Hi Luka,

A simple Rectangle annotation solution. Custom annotations require an annotation class containing the annotation data and render logic and an annotation factory with logic for creating, reading and writing annotations. See below a complete implementation of both classes.

You'll need register the factory with the AnnotationStore and then you can add rectangle annotations like so;

annotationStore.RegisterFactory(new RectAnnotationFactory());
annotationStore.Add(new RectAnnotation {
    PageNumber = 1,
    Fill = Colors.Red,
    Rect = new Rect(100,100,200,200)
});

 

public class RectAnnotation
        : Annotation
    {
        public static readonly XName TypeRect = XName.Get("Rect", AnnotationStore.NamespaceAnnotations);

        public RectAnnotation()
            : base(TypeRect)
        {
        }

        public int PageNumber { get; set; }

        public Rect Rect { get; set; }

        public Color Fill { get; set; }

        public override bool ContainsPage(int pageNumber)
        {
            return this.PageNumber == pageNumber;
        }

        public override void RenderAnnotation(AnnotationRenderContext context)
        {
            var rect = new Rectangle {
                Width = this.Rect.Width,
                Height = this.Rect.Height,
                Fill = new SolidColorBrush(this.Fill)
            };
            Canvas.SetLeft(rect, this.Rect.Left);
            Canvas.SetTop(rect, this.Rect.Top);


            context.AnnotationLayer.Children.Add(rect);
        }
    }
public class RectAnnotationFactory
        : AnnotationFactory<RectAnnotation>
    {
        

        public RectAnnotationFactory()
            : base(RectAnnotation.TypeRect)
        {
        }

        public override RectAnnotation CreateAnnotation()
        {
            return new RectAnnotation();
        }

        protected override RectAnnotation Read(XElement element)
        {
            var result = base.Read(element);
            result.PageNumber = (int)element.Attribute("PageNumber");
            result.Fill = ColorHelper.ColorFromString((string)element.Attribute("Fill")) ?? Colors.Blue;        // fallback to blue
            result.Rect = new Rect {
                X = (double)element.Attribute("X"),
                Y = (double)element.Attribute("Y"),
                Width = (double)element.Attribute("Width"),
                Height = (double)element.Attribute("Height")
            };

            return result;
        }

        protected override XElement Write(RectAnnotation annotation)
        {
            var element = base.Write(annotation);
            element.Add(
                new XAttribute("PageNumber", annotation.PageNumber),
                new XAttribute("Fill", annotation.Fill),
                new XAttribute("X", annotation.Rect.X),
                new XAttribute("Y", annotation.Rect.Y),
                new XAttribute("Width", annotation.Rect.Width),
                new XAttribute("Height", annotation.Rect.Height));

            return element;
        }
    }
Nov 22, 2011 at 2:51 PM

Great, thank you! This does exactly what I wanted, now just to figure out my internal stuff...