Menu

Zoom

Developers
2010-04-29
2013-04-26
  • Codist Monk

    Codist Monk - 2010-04-29

    Here is the problem I'm having.

    I draw a line on the slide and then I use the slider to change the zoom.
    The line doesn't move or scale correctly.

    In the code, the line is rendered directly by Swing, ie the calls to zoomPaint don't do anything.
    Moreover, the code doesn't do a real zoom but emulates it by changing the bounds of the component, which makes it impossible to accurately change the zoom because of the loss of precision when converting doubles to ints.
    Eg: zoom = 1 -> width = 11; zoom = 0.5 -> width = 5; zoom = 1 -> width = 10 instead of 11.

    I tried saving the original bounds but this information needs to be updated when the user moves or resizes the component, but not when it is moved or resized by the zoom emulation algorithm.
    I can see of a way to do that using reflection, but we really shouldn't go that way.

    Even if the save succeeds (or if the user doesn't move the component with the mouse), the positioning of the component still seems off for some zoom values; I suspect this is due to the layout manager, but I'm not sure.

    Bottom line: the rendering should be done differently, because the way it is now makes it quite difficult to implement the zoom correctly.

     
  • Codist Monk

    Codist Monk - 2010-04-29

    I fixed the zoom by modifying the Slide.paint() method so that when the components are rendered, they are scaled according to the zoom factor.

     
  • Codist Monk

    Codist Monk - 2010-05-01

    The problem with this fix is that when the zoom is not 1, the mouse-sensitive areas of the components don't match the display.
    As a consequence, selecting/modifying/creating an object can result in unpleasant visual artifacts if the zoom is not 1.

    Possible solutions:

    • emulate the mouse event dispatching mechanism in Slide;

    • use a new "gizmo component", rendered taking the zoom into account; but that wouldn't solve the problem when creating objects.

     
  • Kyle Flanigan

    Kyle Flanigan - 2010-05-01

    I added repaint() to mouse up and Mose down and that fixed part of the problem. Still issues when adding a new graphicObject and when resizing

     
  • Codist Monk

    Codist Monk - 2010-05-01

    I am going to try to use the frame's glass pane as explained here: http://java.sun.com/docs/books/tutorial/uiswing/components/rootpane.html#glasspane

    The goal is to intercept mouse events before they get to the slide objects, duplicate these events but use a zoom-adjusted position, and dispatch the new events to the appropriate components.

    Here is a demo to illustrate (the important parts are the swing timer updating the slide and the MouseEventRedispatcher class):

    package sandbox;
    import java.awt.BorderLayout;
    import java.awt.Component;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.Point;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.awt.event.MouseEvent;
    import java.awt.event.MouseWheelEvent;
    import javax.swing.JComponent;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.JScrollPane;
    import javax.swing.JSlider;
    import javax.swing.JTextArea;
    import javax.swing.SwingUtilities;
    import javax.swing.WindowConstants;
    import javax.swing.event.MouseInputAdapter;
    /**
     *
     * @author codistmonk (creation 2010-05-01)
     */
    public final class DemoZoom {
        /**
         * Private default constructor to ensure that the class isn't instantiated.
         */
        private DemoZoom() {
            // Nothing
        }
        /**
         *
         * @param arguments
         * <br>Unused
         */
        public static final void main(final String[] arguments) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public final void run() {
                    new MainFrame().setVisible(true);
                }
            });
        }
        /**
         *
         * @author codistmonk (creation 2010-05-01)
         */
        @SuppressWarnings("serial")
        private static final class MainFrame extends JFrame {
            private final Slide slide;
            private final JSlider zoomSlider;
            public MainFrame() {
                this.slide = new Slide(this);
                this.zoomSlider = new JSlider(50, 200, 100);
                this.setGlassPane(new GlassPane(this));
                this.add(new JScrollPane(this.getSlide()), BorderLayout.CENTER);
                this.add(this.getZoomSlider(), BorderLayout.SOUTH);
                this.getGlassPane().setVisible(true);
                this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
                this.pack();
                this.setLocationRelativeTo(null);
            }
            /**
             *
             * @return
             * <br>A non-null value
             * <br>A reference
             */
            public final Slide getSlide() {
                return this.slide;
            }
            /**
             *
             * @return
             * <br>A non-null value
             * <br>A reference
             */
            public final JSlider getZoomSlider() {
                return this.zoomSlider;
            }
        }
        /**
         *
         * @author codistmonk (creation 2010-05-01)
         */
        @SuppressWarnings("serial")
        private static final class Slide extends JPanel {
            private final MainFrame mainFrame;
            /**
             *
             * @param mainFrame
             * <br>Should not be null
             * <br>Reference parameter
             */
            public Slide(final MainFrame mainFrame) {
                super(new BorderLayout());
                this.mainFrame = mainFrame;
                this.add(new JTextArea("Editable text area inside the slide."));
                new javax.swing.Timer(REFRESH_DELAY, new ActionListener() {
                    @Override
                    public final void actionPerformed(final ActionEvent event) {
                        final Double zoom = mainFrame.getZoomSlider().getValue() / 100.0;
                        mainFrame.getSlide().setSize((int) (SLIDE_WIDTH * zoom), (int) (SLIDE_HEIGHT * zoom));
                        mainFrame.getSlide().setPreferredSize(mainFrame.getSlide().getSize());
                        mainFrame.getSlide().repaint();
                    }
                }).start();
            }
            @Override
            public final void paint(final Graphics g) {
                final Double zoom = this.mainFrame.getZoomSlider().getValue() / 100.0;
                ((Graphics2D) g).scale(zoom, zoom);
                super.paint(g);
            }
            public static final int SLIDE_WIDTH = 800;
            public static final int SLIDE_HEIGHT = 600;
            private static final int REFRESH_DELAY = 100;
            
        }
        /**
         *
         * @author codistmonk (creation 2010-05-01)
         */
        @SuppressWarnings("serial")
        private static final class GlassPane extends JComponent {
            /**
             *
             * @param mainFrame
             * <br>Should not be null
             * <br>Reference parameter
             */
            public GlassPane(final MainFrame mainFrame) {
                final MouseEventRedispatcher mouseEventRedispatcher = new MouseEventRedispatcher(mainFrame);
                this.addMouseListener(mouseEventRedispatcher);
                this.addMouseMotionListener(mouseEventRedispatcher);
                this.addMouseWheelListener(mouseEventRedispatcher);
            }
        }
        /**
         *
         * @author codistmonk (creation 2010-05-01)
         */
        private static final class MouseEventRedispatcher extends MouseInputAdapter {
            private final MainFrame mainFrame;
            /**
             *
             * @param mainFrame
             * <br>Should not be null
             * <br>Reference parameter
             */
            public MouseEventRedispatcher(final MainFrame mainFrame) {
                this.mainFrame = mainFrame;
            }
            @Override
            public final void mouseClicked(final MouseEvent event) {
                this.redispatch(event);
            }
            @Override
            public final void mouseDragged(final MouseEvent event) {
                this.redispatch(event);
            }
            @Override
            public final void mouseEntered(final MouseEvent event) {
                this.redispatch(event);
            }
            @Override
            public final void mouseExited(final MouseEvent event) {
                this.redispatch(event);
            }
            @Override
            public final void mouseMoved(final MouseEvent event) {
                this.redispatch(event);
            }
            @Override
            public final void mousePressed(final MouseEvent event) {
                this.redispatch(event);
            }
            @Override
            public final void mouseReleased(final MouseEvent event) {
                this.redispatch(event);
            }
            @Override
            public final void mouseWheelMoved(final MouseWheelEvent event) {
                this.redispatch(event);
            }
            /**
             * @param event
             * <br>Should not be null
             */
            private final void redispatch(final MouseEvent event) {
                final Component component = this.getDestinationComponent(event);
                component.dispatchEvent(this.convertMouseEvent(event, component));
            }
            /**
             *
             * @param event
             * <br>Should not be null
             * @param destinationComponent
             * <br>Should not be null
             * <br>Reference parameter
             * @return
             * <br>A non-null value
             * <br>A new value
             */
            private final MouseEvent convertMouseEvent(final MouseEvent event, final Component destinationComponent) {
                if (SwingUtilities.isDescendingFrom(destinationComponent, this.mainFrame.getSlide())) {
                    final Point zoomedPoint = SwingUtilities.convertPoint(
                            (Component) event.getSource(), event.getPoint(), this.mainFrame.getSlide());
                    final Double zoom = this.mainFrame.getZoomSlider().getValue() / 100.0;
                    return convertMouseEvent(event, destinationComponent,
                            (int) (zoomedPoint.x / zoom), (int) (zoomedPoint.y / zoom));
                }
                
                return SwingUtilities.convertMouseEvent((Component) event.getSource(), event, destinationComponent);
            }
            /**
             *
             * @param event
             * <br>Should not be null
             * @return
             * <br>A non-null value
             * <br>A reference
             */
            private final Component getDestinationComponent(final MouseEvent event) {
                final Component component = SwingUtilities.getDeepestComponentAt(
                        this.mainFrame.getContentPane(), event.getX(), event.getY());
                return component == null ? this.mainFrame.getContentPane() : component;
            }
            /**
             *
             * @param event
             * <br>Should not be null
             * @param destinationComponent
             * <br>Should not be null
             * <br>Reference parameter
             * @param destinationX
             * <br>Range: Any integer
             * @param destinationY
             * <br>Range: Any integer
             * @return
             * <br>A non-null value
             * <br>A new value
             */
            private static final MouseEvent convertMouseEvent(final MouseEvent event,
                    final Component destinationComponent, final int destinationX, final int destinationY) {
                final MouseEvent result;
                if (event instanceof MouseWheelEvent) {
                    result = new MouseWheelEvent(
                        destinationComponent,
                        event.getID(),
                        event.getWhen(),
                        event.getModifiers(),
                        destinationX,
                        destinationY,
                        event.getClickCount(),
                        event.isPopupTrigger(),
                        ((MouseWheelEvent) event).getScrollType(),
                        ((MouseWheelEvent) event).getScrollAmount(),
                        ((MouseWheelEvent) event).getWheelRotation());
                }
                else {
                    result = new MouseEvent(
                        destinationComponent,
                        event.getID(),
                        event.getWhen(),
                        event.getModifiers(),
                        destinationX,
                        destinationY,
                        event.getClickCount(),
                        event.isPopupTrigger(),
                        event.getButton());
                }
                return result;
            }
        }
    }
    
     
  • Codist Monk

    Codist Monk - 2010-05-01

    I wish I could edit my last post, the BBCode got messed up :-(

     
  • Kyle Flanigan

    Kyle Flanigan - 2010-05-02

    I really am impressed with the turnout of your glass pane idea. Very nice job. I did also notice that it seems to take much more time to repaint. I will see if I can help speed that up.
    Kyle

     
  • Kyle Flanigan

    Kyle Flanigan - 2010-05-02

    I forgot to mention that we also need to get the resize of the slide back to how it was. We don't want it getting all out of proportion when the component is resized.
    Kyle

     
  • Codist Monk

    Codist Monk - 2010-05-02

    Some problems:

    • the menus don't work anymore, dispatching the events on the menu bar doesn't seem to work;

    • changing the zoom causes brief visual anomalies, possibly because of the scrollpane's viewport's layout manager.

    To fix the first one, we could use the glass pane only on the slide instead of the whole frame.

    To fix the second one, we could put the slide in a container with a layout manager that doesn't move/resize the child components (layered pane, card layout…).
    When resizing the slide, we resize the container at the same time, and let the viewport layout manager do whatever it wants to the container when it is too small.

     
  • Kyle Flanigan

    Kyle Flanigan - 2010-05-02

    I'm afraid I don't understand exactly what or why you did what is in commit 62. In commit 61 there were no artifacts and I see no visible difference or advantage of the changes in commit 62. Perhaps you could change my mind?
    Kyle

     
  • Codist Monk

    Codist Monk - 2010-05-02

    In revision 61 the scrollbars don't appear when the zoom makes the slide bigger than the viewport (I have a small screen).

     
  • Codist Monk

    Codist Monk - 2010-05-02

    In revision 62 I added a listener to the zoom slider to resize the slide according to the zoom.
    But the viewport layout manager detects the change and also tries to resize the slide.
    As a result, when the zoom is being changed, the slide is drawn twice with different dimensions, hence the visual defect.

     
  • Codist Monk

    Codist Monk - 2010-05-03

    I realized that the layered pane in Slide is the actual slide because it contains the objects handled by the user.

    I suggest to rename Slide into SlideContainer.
    Then, the layered pane should be made into a new class Slide, and the zooming code should be moved there.

    Also, the only way I found to reduce the area covered by he glass pane is by using a layered pane (instead of the frame glassPane property) to host the glass pane and the components under it.
    Possibilities:

    • SlideContainer (currently Slide in revision 64) could extend JLayeredPane instead of JPanel; there doesn't seem to be an easy way to operate this change with the NetBeans GUI builder, so SlideContainer would probably have to be created using the current Slide as a guide;

    • the future Slide (currently a simple layered pane in revision 64) is already a layered pane, so we could add the glass pane to it and change the way z-order is handled for graphic objects.

     

Log in to post a comment.

Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.