/*

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

 */
package org.apache.batik.apps.svgbrowser;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FileDialog;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.awt.print.PrinterException;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FilenameFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Vector;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JToolBar;
import javax.swing.JWindow;
import javax.swing.KeyStroke;
import javax.swing.filechooser.FileFilter;
import javax.swing.text.Document;

import org.apache.batik.anim.dom.SVGOMDocument;
import org.apache.batik.bridge.DefaultExternalResourceSecurity;
import org.apache.batik.bridge.DefaultScriptSecurity;
import org.apache.batik.bridge.EmbededExternalResourceSecurity;
import org.apache.batik.bridge.EmbededScriptSecurity;
import org.apache.batik.bridge.ExternalResourceSecurity;
import org.apache.batik.bridge.NoLoadExternalResourceSecurity;
import org.apache.batik.bridge.NoLoadScriptSecurity;
import org.apache.batik.bridge.RelaxedExternalResourceSecurity;
import org.apache.batik.bridge.RelaxedScriptSecurity;
import org.apache.batik.bridge.ScriptSecurity;
import org.apache.batik.bridge.UpdateManager;
import org.apache.batik.bridge.UpdateManagerEvent;
import org.apache.batik.bridge.UpdateManagerListener;
import org.apache.batik.dom.StyleSheetProcessingInstruction;

import org.apache.batik.dom.util.DOMUtilities;
import org.apache.batik.ext.swing.JAffineTransformChooser;
import org.apache.batik.swing.JSVGCanvas;
import org.apache.batik.swing.gvt.GVTTreeRendererEvent;
import org.apache.batik.swing.gvt.GVTTreeRendererListener;
import org.apache.batik.swing.gvt.Overlay;
import org.apache.batik.swing.svg.GVTTreeBuilderEvent;
import org.apache.batik.swing.svg.GVTTreeBuilderListener;
import org.apache.batik.swing.svg.LinkActivationEvent;
import org.apache.batik.swing.svg.LinkActivationListener;
import org.apache.batik.swing.svg.SVGDocumentLoaderEvent;
import org.apache.batik.swing.svg.SVGDocumentLoaderListener;
import org.apache.batik.swing.svg.SVGFileFilter;
import org.apache.batik.swing.svg.SVGLoadEventDispatcherEvent;
import org.apache.batik.swing.svg.SVGLoadEventDispatcherListener;
import org.apache.batik.swing.svg.SVGUserAgent;
import org.apache.batik.transcoder.TranscoderInput;
import org.apache.batik.transcoder.TranscoderOutput;
import org.apache.batik.transcoder.image.ImageTranscoder;
import org.apache.batik.transcoder.image.JPEGTranscoder;
import org.apache.batik.transcoder.image.PNGTranscoder;
import org.apache.batik.transcoder.image.TIFFTranscoder;
import org.apache.batik.transcoder.print.PrintTranscoder;
import org.apache.batik.transcoder.svg2svg.SVGTranscoder;
import org.apache.batik.util.ParsedURL;
import org.apache.batik.util.Platform;
import org.apache.batik.util.Service;
import org.apache.batik.util.SVGConstants;
import org.apache.batik.constants.XMLConstants;
import org.apache.batik.util.gui.JErrorPane;
import org.apache.batik.util.gui.LocationBar;
import org.apache.batik.util.gui.MemoryMonitor;
import org.apache.batik.util.gui.URIChooser;
import org.apache.batik.util.gui.resource.ActionMap;
import org.apache.batik.util.gui.resource.JComponentModifier;
import org.apache.batik.util.gui.resource.MenuFactory;
import org.apache.batik.util.gui.resource.MissingListenerException;
import org.apache.batik.util.gui.resource.ToolBarFactory;
import org.apache.batik.util.gui.xmleditor.XMLDocument;
import org.apache.batik.util.gui.xmleditor.XMLTextEditor;
import org.apache.batik.util.resources.ResourceManager;
import org.apache.batik.xml.XMLUtilities;

import org.apache.xmlgraphics.util.UnitConv;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.css.ViewCSS;
import org.w3c.dom.svg.SVGDocument;

/**
 * This class represents a SVG viewer swing frame.
 *
 * @author <a href="mailto:stephane@hillion.org">Stephane Hillion</a>
 * @version $Id$
 */
public class JSVGViewerFrame
    extends    JFrame
    implements ActionMap,
               SVGDocumentLoaderListener,
               GVTTreeBuilderListener,
               SVGLoadEventDispatcherListener,
               GVTTreeRendererListener,
               LinkActivationListener,
               UpdateManagerListener {

    private static String EOL;
    static {
        try {
            EOL = System.getProperty("line.separator", "\n");
        } catch (SecurityException e) {
            EOL = "\n";
        }
    }

    /**
     * Kind of ugly, but we need to know if we are running before
     * or after 1.4...
     */
    protected static boolean priorJDK1_4 = true;

    /**
     * If the following class can be found (it appeared in JDK 1.4),
     * then we know we are post JDK 1.4.
     */
    protected static final String JDK_1_4_PRESENCE_TEST_CLASS
        = "java.util.logging.LoggingPermission";

    static {
        try {
            Class.forName(JDK_1_4_PRESENCE_TEST_CLASS);
            priorJDK1_4 = false;
        } catch (ClassNotFoundException e) {
        }
    }

    /**
     * The gui resources file name
     */
    public static final String RESOURCES =
        "org.apache.batik.apps.svgbrowser.resources.GUI";

    // The actions names.
    public static final String ABOUT_ACTION = "AboutAction";
    public static final String OPEN_ACTION = "OpenAction";
    public static final String OPEN_LOCATION_ACTION = "OpenLocationAction";
    public static final String NEW_WINDOW_ACTION = "NewWindowAction";
    public static final String RELOAD_ACTION = "ReloadAction";
    public static final String SAVE_AS_ACTION = "SaveAsAction";
    public static final String BACK_ACTION = "BackAction";
    public static final String FORWARD_ACTION = "ForwardAction";
    public static final String FULL_SCREEN_ACTION = "FullScreenAction";
    public static final String PRINT_ACTION = "PrintAction";
    public static final String EXPORT_AS_JPG_ACTION = "ExportAsJPGAction";
    public static final String EXPORT_AS_PNG_ACTION = "ExportAsPNGAction";
    public static final String EXPORT_AS_TIFF_ACTION = "ExportAsTIFFAction";
    public static final String PREFERENCES_ACTION = "PreferencesAction";
    public static final String CLOSE_ACTION = "CloseAction";
    public static final String VIEW_SOURCE_ACTION = "ViewSourceAction";
    public static final String EXIT_ACTION = "ExitAction";
    public static final String RESET_TRANSFORM_ACTION = "ResetTransformAction";
    public static final String ZOOM_IN_ACTION = "ZoomInAction";
    public static final String ZOOM_OUT_ACTION = "ZoomOutAction";
    public static final String PREVIOUS_TRANSFORM_ACTION = "PreviousTransformAction";
    public static final String NEXT_TRANSFORM_ACTION = "NextTransformAction";
    public static final String USE_STYLESHEET_ACTION = "UseStylesheetAction";
    public static final String PLAY_ACTION = "PlayAction";
    public static final String PAUSE_ACTION = "PauseAction";
    public static final String STOP_ACTION = "StopAction";
    public static final String MONITOR_ACTION = "MonitorAction";
    public static final String DOM_VIEWER_ACTION = "DOMViewerAction";
    public static final String SET_TRANSFORM_ACTION = "SetTransformAction";
    public static final String FIND_DIALOG_ACTION = "FindDialogAction";
    public static final String THUMBNAIL_DIALOG_ACTION = "ThumbnailDialogAction";
    public static final String FLUSH_ACTION = "FlushAction";
    public static final String TOGGLE_DEBUGGER_ACTION = "ToggleDebuggerAction";

    /**
     * The cursor indicating that an operation is pending.
     */
    public static final Cursor WAIT_CURSOR =
        new Cursor(Cursor.WAIT_CURSOR);

    /**
     * The default cursor.
     */
    public static final Cursor DEFAULT_CURSOR =
        new Cursor(Cursor.DEFAULT_CURSOR);

    /**
     * Name for the os-name property
     */
    public static final String PROPERTY_OS_NAME
        = Resources.getString("JSVGViewerFrame.property.os.name");

    /**
     * Name for the os.name default
     */
    public static final String PROPERTY_OS_NAME_DEFAULT
        = Resources.getString("JSVGViewerFrame.property.os.name.default");

    /**
     * Name for the os.name property prefix we are looking
     * for in OpenAction to work around JFileChooser bug
     */
    public static final String PROPERTY_OS_WINDOWS_PREFIX
        = Resources.getString("JSVGViewerFrame.property.os.windows.prefix");

    /**
     * Resource string name for the Open dialog.
     */
    protected static final String OPEN_TITLE = "Open.title";

    /**
     * The input handlers
     */
    protected static Vector handlers;

    /**
     * The default input handler
     */
    protected static SquiggleInputHandler defaultHandler = new SVGInputHandler();

    /**
     * The resource bundle
     */
    protected static ResourceBundle bundle;

    /**
     * The resource manager
     */
    protected static ResourceManager resources;
    static {
        bundle = ResourceBundle.getBundle(RESOURCES, Locale.getDefault());
        resources = new ResourceManager(bundle);
    }

    /**
     * The current application.
     */
    protected Application application;

    /**
     * The JSVGCanvas.
     */
    protected Canvas svgCanvas;

    /**
     * An extension of JSVGCanvas that exposes the Rhino interpreter.
     */
    protected class Canvas extends JSVGCanvas {

        /**
         * Creates a new Canvas.
         */
        public Canvas(SVGUserAgent ua, boolean eventsEnabled,
                      boolean selectableText) {
            super(ua, eventsEnabled, selectableText);
        }

        /**
         * Returns the Rhino interpreter for this canvas.
         */
        public Object getRhinoInterpreter() {
            if (bridgeContext == null) {
                return null;
            }
            return bridgeContext.getInterpreter("text/ecmascript");
        }

        /**
         * JSVGViewerFrame DOMViewerController implementation.
         */
        protected class JSVGViewerDOMViewerController
                // extends CanvasDOMViewerController {
                implements DOMViewerController {

            public boolean canEdit() {
                return getUpdateManager() != null;
            }

            public ElementOverlayManager createSelectionManager() {
                if (canEdit()) {
                    return new ElementOverlayManager(Canvas.this);
                }
                return null;
            }

            public org.w3c.dom.Document getDocument() {
                return Canvas.this.svgDocument;
            }

            public void performUpdate(Runnable r) {
                if (canEdit()) {
                    getUpdateManager().getUpdateRunnableQueue().invokeLater(r);
                } else {
                    r.run();
                }
            }

            public void removeSelectionOverlay(Overlay selectionOverlay) {
                getOverlays().remove(selectionOverlay);
            }

            public void selectNode(Node node) {
                DOMViewerAction dViewerAction =
                    (DOMViewerAction) getAction(DOM_VIEWER_ACTION);
                dViewerAction.openDOMViewer();
                domViewer.selectNode(node);
            }
        }
    }

    /**
     * The panel where the svgCanvas is displayed
     */
    protected JPanel svgCanvasPanel;

    /**
     * A window used for full screen display
     */
    protected JWindow window;

    /**
     * The memory monitor frame.
     */
    protected static JFrame memoryMonitorFrame;

    /**
     * The current path.
     */
    protected File currentPath = new File("");

    /**
     * The current export path.
     */
    protected File currentSavePath = new File("");

    /**
     * The back action
     */
    protected BackAction backAction = new BackAction();

    /**
     * The forward action
     */
    protected ForwardAction forwardAction = new ForwardAction();

    /**
     * The play action
     */
    protected PlayAction playAction = new PlayAction();

    /**
     * The pause action
     */
    protected PauseAction pauseAction = new PauseAction();

    /**
     * The stop action
     */
    protected StopAction stopAction = new StopAction();

    /**
     * The previous transform action
     */
    protected PreviousTransformAction previousTransformAction =
        new PreviousTransformAction();

    /**
     * The next transform action
     */
    protected NextTransformAction nextTransformAction =
        new NextTransformAction();

    /**
     * The use (author) stylesheet action
     */
    protected UseStylesheetAction useStylesheetAction =
        new UseStylesheetAction();

    /**
     * The debug flag.
     */
    protected boolean debug;

    /**
     * The auto adjust flag.
     */
    protected boolean autoAdjust = true;

    /**
     * Whether the update manager was stopped.
     */
    protected boolean managerStopped;

    /**
     * The SVG user agent.
     */
    protected SVGUserAgent userAgent = new UserAgent();

    /**
     * The current document.
     */
    protected SVGDocument svgDocument;

    /**
     * The URI chooser.
     */
    protected URIChooser uriChooser;

    /**
     * The DOM viewer.
     */
    protected DOMViewer domViewer;

    /**
     * The Find dialog.
     */
    protected FindDialog findDialog;

    /**
     * The Find dialog.
     */
    protected ThumbnailDialog thumbnailDialog;

    /**
     * The transform dialog
     */
    protected JAffineTransformChooser.Dialog transformDialog;

    /**
     * The location bar.
     */
    protected LocationBar locationBar;

    /**
     * The status bar.
     */
    protected StatusBar statusBar;

    /**
     * The initial frame title.
     */
    protected String title;

    /**
     * The local history.
     */
    protected LocalHistory localHistory;

    /**
     * The transform history.
     */
    protected TransformHistory transformHistory = new TransformHistory();

    /**
     * The alternate style-sheet title.
     */
    protected String alternateStyleSheet;

    /**
     * The debugger object.
     */
    protected Debugger debugger;

    /**
     * Creates a new SVG viewer frame.
     */
    public JSVGViewerFrame(Application app) {
        application = app;

        addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                application.closeJSVGViewerFrame(JSVGViewerFrame.this);
            }
        });

        //
        // Set the frame's maximum size so that content
        // bigger than the screen does not cause the creation
        // of unnecessary large images.
        //
        svgCanvas = new Canvas(userAgent, true, true) {
                Dimension screenSize;

                {
                    screenSize = Toolkit.getDefaultToolkit().getScreenSize();
                    setMaximumSize(screenSize);
                }

                public Dimension getPreferredSize(){
                    Dimension s = super.getPreferredSize();
                    if (s.width > screenSize.width) s.width =screenSize.width;
                    if (s.height > screenSize.height) s.height = screenSize.height;
                    return s;
                }


                /**
                 * This method is called when the component knows the desired
                 * size of the window (based on width/height of outermost SVG
                 * element). We override it to immediately pack this frame.
                 */
                public void setMySize(Dimension d) {
                    setPreferredSize(d);
                    invalidate();
                    if (autoAdjust) {
                        setExtendedState(getExtendedState() & ~MAXIMIZED_BOTH);
                        pack();
                    }
                }

                public void setDisableInteractions(boolean b) {
                    super.setDisableInteractions(b);

                    // Disable/Enable all our different ways to adjust the
                    // rendering transform (menus, toolbar, thumbnail, keyboard).

                    ((Action)listeners.get(SET_TRANSFORM_ACTION)) .setEnabled(!b);

                    if (thumbnailDialog != null)
                        thumbnailDialog.setInteractionEnabled(!b);
                }
            };

        javax.swing.ActionMap map = svgCanvas.getActionMap();
        map.put(FULL_SCREEN_ACTION, new FullScreenAction());
        javax.swing.InputMap imap = svgCanvas.getInputMap(JComponent.WHEN_FOCUSED);
        KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_F11, 0);
        imap.put(key, FULL_SCREEN_ACTION);

        svgCanvas.setDoubleBufferedRendering(true);

        listeners.put(ABOUT_ACTION, new AboutAction());
        listeners.put(OPEN_ACTION, new OpenAction());
        listeners.put(OPEN_LOCATION_ACTION, new OpenLocationAction());
        listeners.put(NEW_WINDOW_ACTION, new NewWindowAction());
        listeners.put(RELOAD_ACTION, new ReloadAction());
        listeners.put(SAVE_AS_ACTION, new SaveAsAction());
        listeners.put(BACK_ACTION, backAction);
        listeners.put(FORWARD_ACTION, forwardAction);
        listeners.put(PRINT_ACTION, new PrintAction());
        listeners.put(EXPORT_AS_JPG_ACTION, new ExportAsJPGAction());
        listeners.put(EXPORT_AS_PNG_ACTION, new ExportAsPNGAction());
        listeners.put(EXPORT_AS_TIFF_ACTION, new ExportAsTIFFAction());
        listeners.put(PREFERENCES_ACTION, new PreferencesAction());
        listeners.put(CLOSE_ACTION, new CloseAction());
        listeners.put(EXIT_ACTION, application.createExitAction(this));
        listeners.put(VIEW_SOURCE_ACTION, new ViewSourceAction());

        javax.swing.ActionMap cMap = svgCanvas.getActionMap();
        listeners.put(RESET_TRANSFORM_ACTION,
                      cMap.get(JSVGCanvas.RESET_TRANSFORM_ACTION));
        listeners.put(ZOOM_IN_ACTION,
                      cMap.get(JSVGCanvas.ZOOM_IN_ACTION));
        listeners.put(ZOOM_OUT_ACTION,
                      cMap.get(JSVGCanvas.ZOOM_OUT_ACTION));

        listeners.put(PREVIOUS_TRANSFORM_ACTION, previousTransformAction);
        key = KeyStroke.getKeyStroke(KeyEvent.VK_K, KeyEvent.CTRL_DOWN_MASK);
        imap.put(key, previousTransformAction);

        listeners.put(NEXT_TRANSFORM_ACTION, nextTransformAction);
        key = KeyStroke.getKeyStroke(KeyEvent.VK_L, KeyEvent.CTRL_DOWN_MASK);
        imap.put(key, nextTransformAction);

        listeners.put(USE_STYLESHEET_ACTION, useStylesheetAction);
        listeners.put(PLAY_ACTION, playAction);
        listeners.put(PAUSE_ACTION, pauseAction);
        listeners.put(STOP_ACTION, stopAction);
        listeners.put(MONITOR_ACTION, new MonitorAction());
        listeners.put(DOM_VIEWER_ACTION, new DOMViewerAction());
        listeners.put(SET_TRANSFORM_ACTION, new SetTransformAction());
        listeners.put(FIND_DIALOG_ACTION, new FindDialogAction());
        listeners.put(THUMBNAIL_DIALOG_ACTION, new ThumbnailDialogAction());
        listeners.put(FLUSH_ACTION, new FlushAction());
        listeners.put(TOGGLE_DEBUGGER_ACTION, new ToggleDebuggerAction());

        JPanel p = null;
        try {
            // Create the menu
            MenuFactory mf = new MenuFactory(bundle, this);
            JMenuBar mb =
                mf.createJMenuBar("MenuBar", application.getUISpecialization());
            setJMenuBar(mb);

            localHistory = new LocalHistory(mb, this);

            String[] uri = application.getVisitedURIs();
            for (String anUri : uri) {
                if (anUri != null && !"".equals(anUri)) {
                    localHistory.update(anUri);
                }
            }
            p = new JPanel(new BorderLayout());

            // Create the toolbar
            ToolBarFactory tbf = new ToolBarFactory(bundle, this);
            JToolBar tb = tbf.createJToolBar("ToolBar");
            tb.setFloatable(false);
            getContentPane().add(p, BorderLayout.NORTH);
            p.add(tb, BorderLayout.NORTH);
            p.add(new javax.swing.JSeparator(), BorderLayout.CENTER);
            p.add(locationBar = new LocationBar(), BorderLayout.SOUTH);
            locationBar.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));

        } catch (MissingResourceException e) {
            System.out.println(e.getMessage());
            System.exit(0);
        }

        svgCanvasPanel = new JPanel(new BorderLayout());
        svgCanvasPanel.setBorder(BorderFactory.createEtchedBorder());

        svgCanvasPanel.add(svgCanvas, BorderLayout.CENTER);
        p = new JPanel(new BorderLayout());
        p.add(svgCanvasPanel, BorderLayout.CENTER);
        p.add(statusBar = new StatusBar(), BorderLayout.SOUTH);

        getContentPane().add(p, BorderLayout.CENTER);

        svgCanvas.addSVGDocumentLoaderListener(this);
        svgCanvas.addGVTTreeBuilderListener(this);
        svgCanvas.addSVGLoadEventDispatcherListener(this);
        svgCanvas.addGVTTreeRendererListener(this);
        svgCanvas.addLinkActivationListener(this);
        svgCanvas.addUpdateManagerListener(this);

        svgCanvas.addMouseMotionListener(new MouseMotionAdapter() {
                public void mouseMoved(MouseEvent e) {
                    if (svgDocument == null) {
                        statusBar.setXPosition(e.getX());
                        statusBar.setYPosition(e.getY());
                    } else {
                        try {
                            AffineTransform at;
                            at = svgCanvas.getViewBoxTransform();
                            if (at != null) {
                                at = at.createInverse();
                                Point2D p2d =
                                    at.transform(new Point2D.Float(e.getX(), e.getY()),
                                                 null);
                                statusBar.setXPosition((float)p2d.getX());
                                statusBar.setYPosition((float)p2d.getY());
                                return;
                            }
                        } catch (NoninvertibleTransformException ex) {
                        }
                        statusBar.setXPosition(e.getX());
                        statusBar.setYPosition(e.getY());
                    }
                }
            });
        svgCanvas.addMouseListener(new MouseAdapter() {
                public void mouseExited(MouseEvent e) {
                    Dimension dim = svgCanvas.getSize();
                    if (svgDocument == null) {
                        statusBar.setWidth(dim.width);
                        statusBar.setHeight(dim.height);
                    } else {
                        try {
                            AffineTransform at;
                            at = svgCanvas.getViewBoxTransform();
                            if (at != null) {
                                at = at.createInverse();
                                Point2D o =
                                    at.transform(new Point2D.Float(0, 0),
                                                 null);
                                Point2D p2d =
                                    at.transform(new Point2D.Float(dim.width,
                                                                   dim.height),
                                                 null);
                                statusBar.setWidth((float)(p2d.getX() - o.getX()));
                                statusBar.setHeight((float)(p2d.getY() - o.getY()));
                                return;
                            }
                        } catch (NoninvertibleTransformException ex) {
                        }
                        statusBar.setWidth(dim.width);
                        statusBar.setHeight(dim.height);
                    }
                }
            });
        svgCanvas.addComponentListener(new ComponentAdapter() {
                public void componentResized(ComponentEvent e) {
                    Dimension dim = svgCanvas.getSize();
                    if (svgDocument == null) {
                        statusBar.setWidth(dim.width);
                        statusBar.setHeight(dim.height);
                    } else {
                        try {
                            AffineTransform at;
                            at = svgCanvas.getViewBoxTransform();
                            if (at != null) {
                                at = at.createInverse();
                                Point2D o =
                                    at.transform(new Point2D.Float(0, 0),
                                                 null);
                                Point2D p2d =
                                    at.transform(new Point2D.Float(dim.width,
                                                                   dim.height),
                                                 null);
                                statusBar.setWidth((float)(p2d.getX() - o.getX()));
                                statusBar.setHeight((float)(p2d.getY() - o.getY()));
                                return;
                            }
                        } catch (NoninvertibleTransformException ex) {
                        }
                        statusBar.setWidth(dim.width);
                        statusBar.setHeight(dim.height);
                    }
                }
            });

        locationBar.addActionListener(new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                String st = locationBar.getText().trim();
                int i = st.indexOf( '#' );
                String t = "";
                if (i != -1) {
                    t = st.substring(i + 1);
                    st = st.substring(0, i);
                }

                if (st.equals(""))
                    return;

                try{
                    File f = new File(st);
                    if (f.exists()) {
                        if (f.isDirectory()) {
                            return;
                        } else {
                            try {
                                st = f.getCanonicalPath();
                                if (st.startsWith("/")) {
                                    st = "file:" + st;
                                } else {
                                    st = "file:/" + st;
                                }
                            } catch (IOException ex) {
                            }
                        }
                    }
                }catch(SecurityException se){
                    // Could not patch the file URI for security
                    // reasons (e.g., when run as an unsigned
                    // JavaWebStart jar): file access is not
                    // allowed. Loading will fail, but there is
                    // nothing more to do at this point.
                }

                String fi = svgCanvas.getFragmentIdentifier();
                if (svgDocument != null) {
                    ParsedURL docPURL
                        = new ParsedURL(svgDocument.getURL());
                    ParsedURL purl = new ParsedURL(docPURL, st);
                    fi = (fi == null) ? "" : fi;
                    if (docPURL.equals(purl) && t.equals(fi)) {
                        return;
                    }
                }
                if (t.length() != 0) {
                    st += '#' + t;
                }
                locationBar.setText(st);
                locationBar.addToHistory(st);
                showSVGDocument(st);
            }
        });
    }

    /**
     * Call dispose on canvas as well.
     */
    public void dispose() {
        hideDebugger();
        svgCanvas.dispose();
        super.dispose();
    }

    /**
     * Whether to show the debug traces.
     */
    public void setDebug(boolean b) {
        debug = b;
    }

    /**
     * Whether to auto adjust the canvas to the size of the document.
     */
    public void setAutoAdjust(boolean b) {
        autoAdjust = b;
    }

    /**
     * Returns the main JSVGCanvas of this frame.
     */
    public JSVGCanvas getJSVGCanvas() {
        return svgCanvas;
    }

    /**
     * Needed to work-around JFileChooser bug with abstract Files
     */
    private static File makeAbsolute(File f){
        if(!f.isAbsolute()){
            return f.getAbsoluteFile();
        }
        return f;
    }

    /**
     * Shows the Rhino debugger.
     */
    public void showDebugger() {
        if (debugger == null && Debugger.isPresent) {
            debugger = new Debugger(this, locationBar.getText());
            debugger.initialize();
        }
    }

    /**
     * Hides and destroys the Rhino debugger.
     */
    public void hideDebugger() {
        if (debugger != null) {
            debugger.clearAllBreakpoints();
            debugger.go();
            debugger.dispose();
            debugger = null;
        }
    }

    /**
     * Rhino debugger class.
     */
    protected static class Debugger {

        /**
         * Whether the Rhino debugger classes are present.
         */
        protected static boolean isPresent;

        /**
         * The Rhino debugger class.
         */
        protected static Class debuggerClass;

        /**
         * The Rhino ContextFactory class.
         */
        protected static Class contextFactoryClass;

        // Indexes into the debuggerMethods array.
        protected static final int CLEAR_ALL_BREAKPOINTS_METHOD = 0;
        protected static final int GO_METHOD                    = 1;
        protected static final int SET_EXIT_ACTION_METHOD       = 2;
        protected static final int ATTACH_TO_METHOD             = 3;
        protected static final int DETACH_METHOD                = 4;
        protected static final int DISPOSE_METHOD               = 5;
        protected static final int GET_DEBUG_FRAME_METHOD       = 6;

        /**
         * Rhino debugger class constructor.
         */
        protected static Constructor debuggerConstructor;

        /**
         * Rhino debugger class methods.
         */
        protected static Method[] debuggerMethods;

        /**
         * The RhinoInterpreter class.
         */
        protected static Class rhinoInterpreterClass;

        /**
         * The {@code getContextFactory} method on the {@link
         * org.apache.batik.bridge.RhinoInterpreter} class.
         */
        protected static Method getContextFactoryMethod;

        static {
            try {
                Class dc =
                    Class.forName("org.mozilla.javascript.tools.debugger.Main");
                Class cfc =
                    Class.forName("org.mozilla.javascript.ContextFactory");
                rhinoInterpreterClass = Class.forName
                    ("org.apache.batik.script.rhino.RhinoInterpreter");
                debuggerConstructor =
                    dc.getConstructor(new Class[] { String.class });
                debuggerMethods = new Method[] {
                    dc.getMethod("clearAllBreakpoints", (Class[]) null),
                    dc.getMethod("go", (Class[]) null),
                    dc.getMethod("setExitAction", new Class[] {Runnable.class}),
                    dc.getMethod("attachTo", new Class[] { cfc }),
                    dc.getMethod("detach", (Class[]) null),
                    dc.getMethod("dispose", (Class[]) null),
                    dc.getMethod("getDebugFrame", (Class[]) null)
                };
                getContextFactoryMethod =
                    rhinoInterpreterClass.getMethod("getContextFactory",
                                                    (Class[]) null);
                debuggerClass = dc;
                isPresent = true;
            } catch (ClassNotFoundException cnfe) {
            } catch (NoSuchMethodException nsme) {
            } catch (SecurityException se) {
            }
        }

        /**
         * The Rhino debugger instance.
         */
        protected Object debuggerInstance;

        /**
         * The JSVGViewerFrame.
         */
        protected JSVGViewerFrame svgFrame;

        /**
         * Creates a new Debugger.
         */
        public Debugger(JSVGViewerFrame frame, String url) {
            svgFrame = frame;
            try {
                debuggerInstance = debuggerConstructor.newInstance
                    (new Object[] { "JavaScript Debugger - " + url });
            } catch (IllegalAccessException iae) {
                throw new RuntimeException(iae.getMessage());
            } catch (InvocationTargetException ite) {
                ite.printStackTrace();
                throw new RuntimeException(ite.getMessage());
            } catch (InstantiationException ie) {
                throw new RuntimeException(ie.getMessage());
            }
        }

        /**
         * Sets the document URL to use in the window title.
         */
        public void setDocumentURL(String url) {
            getDebugFrame().setTitle("JavaScript Debugger - " + url);
        }

        /**
         * Initializes the debugger by massaging the GUI and attaching it
         * to the Rhino interpreter's
         * org.mozilla.javascript.ContextFactory.
         */
        public void initialize() {
            // Customize the menubar a bit, disable menu
            // items that can't be used and change 'Exit' to 'Close'.
            JFrame   debugGui = getDebugFrame();
            JMenuBar menuBar  = debugGui.getJMenuBar();
            JMenu    menu     = menuBar.getMenu(0);
            menu.getItem(0).setEnabled(false); // Open...
            menu.getItem(1).setEnabled(false); // Run...
            menu.getItem(3).setText
                (Resources.getString("Close.text")); // Exit -> "Close"
            menu.getItem(3).setAccelerator
                (KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.CTRL_DOWN_MASK));

            debugGui.setSize(600, 460);
            debugGui.pack();
            setExitAction(new Runnable() {
                    public void run() {
                        svgFrame.hideDebugger();
                    }});
            WindowAdapter wa = new WindowAdapter() {
                    public void windowClosing(WindowEvent e) {
                        svgFrame.hideDebugger();
                    }};
            debugGui.addWindowListener(wa);
            debugGui.setVisible(true);
            attach();
        }

        /**
         * Attaches the debugger to the canvas' current interpreter.
         */
        public void attach() {
            Object interpreter = svgFrame.svgCanvas.getRhinoInterpreter();
            if (interpreter != null) {
                attachTo(getContextFactory(interpreter));
            }
        }

        /**
         * Calls {@code getDebugFrame} on {@link #debuggerInstance}.
         */
        protected JFrame getDebugFrame() {
            try {
                return (JFrame) debuggerMethods[GET_DEBUG_FRAME_METHOD].invoke
                    (debuggerInstance, (Object[]) null);
            } catch (InvocationTargetException ite) {
                throw new RuntimeException(ite.getMessage());
            } catch (IllegalAccessException iae) {
                throw new RuntimeException(iae.getMessage());
            }
        }

        /**
         * Calls {@code setExitAction} on {@link #debuggerInstance}.
         */
        protected void setExitAction(Runnable r) {
            try {
                debuggerMethods[SET_EXIT_ACTION_METHOD].invoke
                    (debuggerInstance, new Object[] { r });
            } catch (InvocationTargetException ite) {
                throw new RuntimeException(ite.getMessage());
            } catch (IllegalAccessException iae) {
                throw new RuntimeException(iae.getMessage());
            }
        }

        /**
         * Calls {@code attachTo} on {@link #debuggerInstance}.
         */
        public void attachTo(Object contextFactory) {
            try {
                debuggerMethods[ATTACH_TO_METHOD].invoke
                    (debuggerInstance, new Object[] { contextFactory });
            } catch (InvocationTargetException ite) {
                throw new RuntimeException(ite.getMessage());
            } catch (IllegalAccessException iae) {
                throw new RuntimeException(iae.getMessage());
            }
        }

        /**
         * Calls {@code detach} on {@link #debuggerInstance}.
         */
        public void detach() {
            try {
                debuggerMethods[DETACH_METHOD].invoke(debuggerInstance,
                                                      (Object[]) null);
            } catch (InvocationTargetException ite) {
                throw new RuntimeException(ite.getMessage());
            } catch (IllegalAccessException iae) {
                throw new RuntimeException(iae.getMessage());
            }
        }

        /**
         * Calls {@code go} on {@link #debuggerInstance}.
         */
        public void go() {
            try {
                debuggerMethods[GO_METHOD].invoke(debuggerInstance,
                                                  (Object[]) null);
            } catch (InvocationTargetException ite) {
                throw new RuntimeException(ite.getMessage());
            } catch (IllegalAccessException iae) {
                throw new RuntimeException(iae.getMessage());
            }
        }

        /**
         * Calls {@code clearAllBreakpoints} on {@link #debuggerInstance}.
         */
        public void clearAllBreakpoints() {
            try {
                debuggerMethods[CLEAR_ALL_BREAKPOINTS_METHOD].invoke
                    (debuggerInstance, (Object[]) null);
            } catch (InvocationTargetException ite) {
                throw new RuntimeException(ite.getMessage());
            } catch (IllegalAccessException iae) {
                throw new RuntimeException(iae.getMessage());
            }
        }

        /**
         * Calls {@code dispose} on {@link #debuggerInstance}.
         */
        public void dispose() {
            try {
                debuggerMethods[DISPOSE_METHOD].invoke(debuggerInstance,
                                                       (Object[]) null);
            } catch (InvocationTargetException ite) {
                throw new RuntimeException(ite.getMessage());
            } catch (IllegalAccessException iae) {
                throw new RuntimeException(iae.getMessage());
            }
        }

        /**
         * Calls {@code getContextFactory} on the given instance of
         * {@link org.apache.batik.bridge.RhinoInterpreter}.
         */
        protected Object getContextFactory(Object rhinoInterpreter) {
            try {
                return getContextFactoryMethod.invoke(rhinoInterpreter,
                                                      (Object[]) null);
            } catch (InvocationTargetException ite) {
                throw new RuntimeException(ite.getMessage());
            } catch (IllegalAccessException iae) {
                throw new RuntimeException(iae.getMessage());
            }
        }
    }

    /**
     * To show the about dialog
     */
    public class AboutAction extends AbstractAction {
        public AboutAction(){
        }

        public void actionPerformed(ActionEvent e){
            AboutDialog dlg = new AboutDialog(JSVGViewerFrame.this);
            // Work around pack() bug on some platforms
            dlg.setSize(dlg.getPreferredSize());
            dlg.setLocationRelativeTo(JSVGViewerFrame.this);
            dlg.setVisible(true);
            dlg.toFront();
        }
    }

    /**
     * To open a new file.
     */
    public class OpenAction extends AbstractAction {

        public OpenAction() {
        }
        public void actionPerformed(ActionEvent e) {
            File f = null;
            if (Platform.isOSX) {
                FileDialog fileDialog =
                    new FileDialog(JSVGViewerFrame.this,
                                   Resources.getString(OPEN_TITLE));
                fileDialog.setFilenameFilter(new FilenameFilter() {
                    public boolean accept(File dir, String name) {
                        for (Object o : getHandlers()) {
                            SquiggleInputHandler handler
                                    = (SquiggleInputHandler) o;
                            if (handler.accept(new File(dir, name))) {
                                return true;
                            }
                        }
                        return false;
                    }
                });
                fileDialog.setVisible(true);
                String filename = fileDialog.getFile();
                if (fileDialog != null) {
                    String dirname = fileDialog.getDirectory();
                    f = new File(dirname, filename);
                }
            } else {
                JFileChooser fileChooser = null;

                // Apply work around Windows problem when security is enabled,
                // and when prior to JDK 1.4.
                String os = System.getProperty(PROPERTY_OS_NAME, PROPERTY_OS_NAME_DEFAULT);
                SecurityManager sm = System.getSecurityManager();

                if ( priorJDK1_4 && sm != null && os.indexOf(PROPERTY_OS_WINDOWS_PREFIX) != -1 ){
                    fileChooser = new JFileChooser(makeAbsolute(currentPath),
                                                   new WindowsAltFileSystemView());
                } else {
                    fileChooser = new JFileChooser(makeAbsolute(currentPath));
                }

                fileChooser.setFileHidingEnabled(false);
                fileChooser.setFileSelectionMode
                    (JFileChooser.FILES_ONLY);

                //
                // Add file filters from the handlers map
                //
                for (Object o : getHandlers()) {
                    SquiggleInputHandler handler
                            = (SquiggleInputHandler) o;
                    fileChooser.addChoosableFileFilter
                            (new SquiggleInputHandlerFilter(handler));
                }

                int choice = fileChooser.showOpenDialog(JSVGViewerFrame.this);
                if (choice == JFileChooser.APPROVE_OPTION) {
                    f = fileChooser.getSelectedFile();
                    currentPath = f;
                }
            }

            if (f != null) {
                try {
                    String furl = f.toURI().toURL().toString();
                    showSVGDocument(furl);
                } catch (MalformedURLException ex) {
                    if (userAgent != null) {
                        userAgent.displayError(ex);
                    }
                }
            }
        }
    }

    /**
     * Shows the given document into the viewer frame
     */
    public void showSVGDocument(String uri){
        try {
            ParsedURL purl = new ParsedURL(uri);
            SquiggleInputHandler
                handler = getInputHandler(purl);

            handler.handle(purl,
                           JSVGViewerFrame.this);
        } catch (Exception e) {
            if (userAgent != null) {
                userAgent.displayError(e);
            }
        }

    }

    /**
     * Returns the input handler for the given URI
     */
    public SquiggleInputHandler getInputHandler(ParsedURL purl) throws IOException {
        Iterator iter = getHandlers().iterator();
        SquiggleInputHandler handler = null;

        while (iter.hasNext()) {
            SquiggleInputHandler curHandler =
                (SquiggleInputHandler)iter.next();
            if (curHandler.accept(purl)) {
                handler = curHandler;
                break;
            }
        }

        // No handler found, use the default one.
        if (handler == null) {
            handler = defaultHandler;
        }

        return handler;
    }


    /**
     * Returns the list of input file handler.
     */
    protected static Vector getHandlers() {
        if (handlers != null) {
            return handlers;
        }

        handlers = new Vector();
        registerHandler(new SVGInputHandler());

        Iterator iter = Service.providers(SquiggleInputHandler.class);
        while (iter.hasNext()) {
            SquiggleInputHandler handler
                = (SquiggleInputHandler)iter.next();

            registerHandler(handler);
        }

        return handlers;
    }

    /**
     * Registers an input file handler by adding it to the handlers map.
     * @param handler the new input handler to register.
     */
    public static synchronized
        void registerHandler(SquiggleInputHandler handler) {
        Vector handlers = getHandlers();
        handlers.addElement(handler);
    }

    /**
     * To open a new document.
     */
    public class OpenLocationAction extends AbstractAction {
        public OpenLocationAction() {}
        public void actionPerformed(ActionEvent e) {
            if (uriChooser == null) {
                uriChooser = new URIChooser(JSVGViewerFrame.this);
                uriChooser.setFileFilter(new SVGFileFilter());
                uriChooser.pack();
                Rectangle fr = getBounds();
                Dimension sd = uriChooser.getSize();
                uriChooser.setLocation(fr.x + (fr.width  - sd.width) / 2,
                                       fr.y + (fr.height - sd.height) / 2);
            }
            if (uriChooser.showDialog() == URIChooser.OK_OPTION) {
                String s = uriChooser.getText();
                if (s == null) return;
                int i = s.indexOf( '#' );
                String t = "";
                if (i != -1) {
                    t = s.substring(i + 1);
                    s = s.substring(0, i);
                }
                if (!s.equals("")) {
                    File f = new File(s);
                    if (f.exists()) {
                        if (f.isDirectory()) {
                            s = null;
                        } else {
                            try {
                                s = f.getCanonicalPath();
                                if (s.startsWith("/")) {
                                    s = "file:" + s;
                                } else {
                                    s = "file:/" + s;
                                }
                            } catch (IOException ex) {
                            }
                        }
                    }
                    if (s != null) {
                        if (svgDocument != null) {
                            ParsedURL docPURL
                                = new ParsedURL(svgDocument.getURL());
                            ParsedURL purl = new ParsedURL(docPURL, s);
                            String fi = svgCanvas.getFragmentIdentifier();
                            if (docPURL.equals(purl) && t.equals(fi)) {
                                return;
                            }
                        }
                        if (t.length() != 0) {
                            s += '#' + t;
                        }

                        showSVGDocument(s);
                    }
                }
            }
        }
    }

    /**
     * To open a new window.
     */
    public class NewWindowAction extends AbstractAction {
        public NewWindowAction() {}
        public void actionPerformed(ActionEvent e) {
            JSVGViewerFrame vf = application.createAndShowJSVGViewerFrame();

            // Copy the current settings to the new window.
            vf.autoAdjust = autoAdjust;
            vf.debug = debug;
            vf.svgCanvas.setProgressivePaint(svgCanvas.getProgressivePaint());
            vf.svgCanvas.setDoubleBufferedRendering
                (svgCanvas.getDoubleBufferedRendering());
        }
    }

    /**
     * To show the preferences.
     */
    public class PreferencesAction extends AbstractAction {
        public PreferencesAction() {}
        public void actionPerformed(ActionEvent e) {
            application.showPreferenceDialog(JSVGViewerFrame.this);
        }
    }

    /**
     * To close the last document.
     */
    public class CloseAction extends AbstractAction {
        public CloseAction() {}
        public void actionPerformed(ActionEvent e) {
            application.closeJSVGViewerFrame(JSVGViewerFrame.this);
        }
    }

    /**
     * To reload the current document.
     */
    public class ReloadAction extends AbstractAction {
        public ReloadAction() {}
        public void actionPerformed(ActionEvent e) {
            if ((e.getModifiers() & ActionEvent.SHIFT_MASK) == 1) {
                svgCanvas.flushImageCache();
            }
            if (svgDocument != null) {
                localHistory.reload();
            }
        }
    }

    /**
     * To go back to the previous document
     */
    public class BackAction extends    AbstractAction
                            implements JComponentModifier {
        List components = new LinkedList();
        public BackAction() {}
        public void actionPerformed(ActionEvent e) {
            if (localHistory.canGoBack()) {
                localHistory.back();
            }
        }

        public void addJComponent(JComponent c) {
            components.add(c);
            c.setEnabled(false);
        }

        protected void update() {
            boolean b = localHistory.canGoBack();
            for (Object component : components) {
                ((JComponent) component).setEnabled(b);
            }
        }
    }

    /**
     * To go forward to the next document
     */
    public class ForwardAction extends    AbstractAction
                               implements JComponentModifier {
        List components = new LinkedList();
        public ForwardAction() {}
        public void actionPerformed(ActionEvent e) {
            if (localHistory.canGoForward()) {
                localHistory.forward();
            }
        }

        public void addJComponent(JComponent c) {
            components.add(c);
            c.setEnabled(false);
        }

        protected void update() {
            boolean b = localHistory.canGoForward();
            for (Object component : components) {
                ((JComponent) component).setEnabled(b);
            }
        }
    }

    /**
     * To print the current document.
     */
    public class PrintAction extends AbstractAction {
        public PrintAction() {}
        public void actionPerformed(ActionEvent e) {
            if (svgDocument != null) {
                final SVGDocument doc = svgDocument;
                new Thread() {
                    public void run(){
                        String uri = doc.getURL();
                        String fragment = svgCanvas.getFragmentIdentifier();
                        if (fragment != null) {
                            uri += '#' +fragment;
                        }

                        //
                        // Build a PrintTranscoder to handle printing
                        // of the svgDocument object
                        //
                        PrintTranscoder pt = new PrintTranscoder();

                        //
                        // Set transcoding hints
                        //
                        if (application.getXMLParserClassName() != null) {
                            pt.addTranscodingHint
                                (JPEGTranscoder.KEY_XML_PARSER_CLASSNAME,
                                    application.getXMLParserClassName());
                        }

                        pt.addTranscodingHint(PrintTranscoder.KEY_SHOW_PAGE_DIALOG,
                                              Boolean.TRUE);


                        pt.addTranscodingHint(PrintTranscoder.KEY_SHOW_PRINTER_DIALOG,
                                              Boolean.TRUE);

                        //
                        // Do transcoding now
                        //
                        pt.transcode(new TranscoderInput(uri), null);

                        //
                        // Print
                        //
                        try {
                            pt.print();
                        } catch (PrinterException ex) {
                            userAgent.displayError(ex);
                        }
                    }
                }.start();
            }
        }
    }

    /**
     * To save the current document as SVG.
     */
    public class SaveAsAction extends AbstractAction {
        public SaveAsAction() {}

        public void actionPerformed(ActionEvent e) {
            JFileChooser fileChooser;
            fileChooser = new JFileChooser(makeAbsolute(currentSavePath));
            fileChooser.setDialogTitle(resources.getString("SaveAs.title"));
            fileChooser.setFileHidingEnabled(false);
            fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
            fileChooser.addChoosableFileFilter(new ImageFileFilter(".svg"));

            int choice = fileChooser.showSaveDialog(JSVGViewerFrame.this);
            if (choice != JFileChooser.APPROVE_OPTION)
                return;

            final File f = fileChooser.getSelectedFile();

            SVGOptionPanel sop;
            sop = SVGOptionPanel.showDialog(JSVGViewerFrame.this);

            final boolean useXMLBase  = sop.getUseXMLBase();
            final boolean prettyPrint = sop.getPrettyPrint();
            sop = null;

            final SVGDocument svgDoc = svgCanvas.getSVGDocument();
            if (svgDoc == null) return;

            statusBar.setMessage(resources.getString("Message.saveAs"));
            currentSavePath = f;
            OutputStreamWriter w = null;
            try {
                OutputStream tos = null;
                tos = new FileOutputStream(f);
                tos = new BufferedOutputStream(tos);
                w = new OutputStreamWriter(tos, "utf-8");
            } catch (Exception ex) {
                userAgent.displayError(ex);
                return;
            }

            final OutputStreamWriter writer  = w;

            final Runnable doneRun = new Runnable() {
                    public void run() {
                        String doneStr = resources.getString("Message.done");
                        statusBar.setMessage(doneStr);
                    }
                };
            Runnable r = new Runnable() {
                    public void run() {
                        try {
                            // Write standard XML header.
                            writer.write
                                ("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
                                writer.write (EOL);

                            Node fc = svgDoc.getFirstChild();
                            if (fc.getNodeType() != Node.DOCUMENT_TYPE_NODE) {
                                // Not DT node in Document, so
                                // provide Document Type dec.
                                writer.write ("<!DOCTYPE svg PUBLIC '");
                                writer.write (SVGConstants.SVG_PUBLIC_ID);
                                writer.write ("' '");
                                writer.write (SVGConstants.SVG_SYSTEM_ID);
                                writer.write ("'>");
                                writer.write (EOL);
                                writer.write (EOL);
                            }
                            Element root = svgDoc.getRootElement();
                            boolean doXMLBase = useXMLBase;
                            if (root.hasAttributeNS
                                (XMLConstants.XML_NAMESPACE_URI, "base"))
                                doXMLBase = false;

                            if (doXMLBase) {
                                root.setAttributeNS
                                    (XMLConstants.XML_NAMESPACE_URI,
                                     "xml:base",
                                     svgDoc.getURL());
                            }

                            if (prettyPrint) {
                                SVGTranscoder trans = new SVGTranscoder();
                                trans.transcode(new TranscoderInput(svgDoc),
                                                new TranscoderOutput(writer));
                            } else {
                                DOMUtilities.writeDocument(svgDoc, writer);
                            }

                            writer.close();

                            if (doXMLBase)
                                root.removeAttributeNS
                                    (XMLConstants.XML_NAMESPACE_URI,
                                     "xml:base");

                            if (EventQueue.isDispatchThread()) {
                                doneRun.run();
                            } else {
                                EventQueue.invokeLater(doneRun);
                            }
                        } catch (Exception ex) {
                            userAgent.displayError(ex);
                        }
                    }
                };

            UpdateManager um = svgCanvas.getUpdateManager();
            if ((um != null) && (um.isRunning())) {
                um.getUpdateRunnableQueue().invokeLater(r);
            } else {
                r.run();
            }
        }
    }

    /**
     * To save the current document as JPG.
     */
    public class ExportAsJPGAction extends AbstractAction {
        public ExportAsJPGAction() {}
        public void actionPerformed(ActionEvent e) {
            JFileChooser fileChooser =
                new JFileChooser(makeAbsolute(currentSavePath));
            fileChooser.setDialogTitle(resources.getString("ExportAsJPG.title"));
            fileChooser.setFileHidingEnabled(false);
            fileChooser.setFileSelectionMode
                (JFileChooser.FILES_ONLY);
            fileChooser.addChoosableFileFilter(new ImageFileFilter(".jpg"));

            int choice = fileChooser.showSaveDialog(JSVGViewerFrame.this);
            if (choice == JFileChooser.APPROVE_OPTION) {
                float quality =
                    JPEGOptionPanel.showDialog(JSVGViewerFrame.this);

                final File f = fileChooser.getSelectedFile();
                BufferedImage buffer = svgCanvas.getOffScreen();
                if (buffer != null) {
                    statusBar.setMessage
                        (resources.getString("Message.exportAsJPG"));

                    // create a BufferedImage of the appropriate type
                    int w = buffer.getWidth();
                    int h = buffer.getHeight();
                    final ImageTranscoder trans = new JPEGTranscoder();
                    if (application.getXMLParserClassName() != null) {
                        trans.addTranscodingHint
                            (JPEGTranscoder.KEY_XML_PARSER_CLASSNAME,
                                application.getXMLParserClassName());
                    }
                    trans.addTranscodingHint
                        (JPEGTranscoder.KEY_QUALITY, quality);

                    final BufferedImage img = trans.createImage(w, h);

                    // paint the buffer to the image
                    Graphics2D g2d = img.createGraphics();
                    g2d.setColor(Color.white);
                    g2d.fillRect(0, 0, w, h);
                    g2d.drawImage(buffer, null, 0, 0);
                    new Thread() {
                        public void run() {
                            try {
                                currentSavePath = f;
                                OutputStream ostream =
                                    new BufferedOutputStream(new FileOutputStream(f));
                                trans.writeImage(img, new TranscoderOutput(ostream));
                                ostream.close();
                            } catch (Exception ex) { }
                            statusBar.setMessage
                                (resources.getString("Message.done"));
                        }
                    }.start();
                }
            }
        }
    }

    /**
     * To save the current document as PNG.
     */
    public class ExportAsPNGAction extends AbstractAction {
        public ExportAsPNGAction() {}
        public void actionPerformed(ActionEvent e) {
            JFileChooser fileChooser =
                new JFileChooser(makeAbsolute(currentSavePath));
            fileChooser.setDialogTitle(resources.getString("ExportAsPNG.title"));
            fileChooser.setFileHidingEnabled(false);
            fileChooser.setFileSelectionMode
                (JFileChooser.FILES_ONLY);
            fileChooser.addChoosableFileFilter(new ImageFileFilter(".png"));

            int choice = fileChooser.showSaveDialog(JSVGViewerFrame.this);
            if (choice == JFileChooser.APPROVE_OPTION) {
                boolean isIndexed = PNGOptionPanel.showDialog(JSVGViewerFrame.this);

                final File f = fileChooser.getSelectedFile();
                BufferedImage buffer = svgCanvas.getOffScreen();
                if (buffer != null) {
                    statusBar.setMessage
                        (resources.getString("Message.exportAsPNG"));

                    // create a BufferedImage of the appropriate type
                    int w = buffer.getWidth();
                    int h = buffer.getHeight();
                    final ImageTranscoder trans = new PNGTranscoder();
                    if (application.getXMLParserClassName() != null) {
                        trans.addTranscodingHint
                            (JPEGTranscoder.KEY_XML_PARSER_CLASSNAME,
                                application.getXMLParserClassName());
                    }
                    trans.addTranscodingHint(PNGTranscoder.KEY_FORCE_TRANSPARENT_WHITE,
                                             Boolean.TRUE );

                    if(isIndexed){
                        trans.addTranscodingHint(PNGTranscoder.KEY_INDEXED, 8);
                    }

                    final BufferedImage img = trans.createImage(w, h);

                    // paint the buffer to the image
                    Graphics2D g2d = img.createGraphics();
                    g2d.drawImage(buffer, null, 0, 0);
                    new Thread() {
                        public void run() {
                            try {
                                currentSavePath = f;
                                OutputStream ostream =
                                    new BufferedOutputStream(new FileOutputStream(f));
                                trans.writeImage(img,
                                                 new TranscoderOutput(ostream));
                                ostream.close();
                            } catch (Exception ex) {}
                            statusBar.setMessage
                                (resources.getString("Message.done"));
                        }
                    }.start();
                }
            }
        }
    }

    /**
     * To save the current document as TIFF.
     */
    public class ExportAsTIFFAction extends AbstractAction {
        public ExportAsTIFFAction() {}
        public void actionPerformed(ActionEvent e) {
            JFileChooser fileChooser =
                new JFileChooser(makeAbsolute(currentSavePath));
            fileChooser.setDialogTitle(resources.getString("ExportAsTIFF.title"));
            fileChooser.setFileHidingEnabled(false);
            fileChooser.setFileSelectionMode
                (JFileChooser.FILES_ONLY);
            fileChooser.addChoosableFileFilter(new ImageFileFilter(".tiff"));

            int choice = fileChooser.showSaveDialog(JSVGViewerFrame.this);
            if (choice == JFileChooser.APPROVE_OPTION) {
                final File f = fileChooser.getSelectedFile();
                BufferedImage buffer = svgCanvas.getOffScreen();
                if (buffer != null) {
                    statusBar.setMessage
                        (resources.getString("Message.exportAsTIFF"));

                    // create a BufferedImage of the appropriate type
                    int w = buffer.getWidth();
                    int h = buffer.getHeight();
                    final ImageTranscoder trans = new TIFFTranscoder();
                    if (application.getXMLParserClassName() != null) {
                        trans.addTranscodingHint
                            (JPEGTranscoder.KEY_XML_PARSER_CLASSNAME,
                                application.getXMLParserClassName());
                    }
                    final BufferedImage img = trans.createImage(w, h);

                    // paint the buffer to the image
                    Graphics2D g2d = img.createGraphics();
                    g2d.drawImage(buffer, null, 0, 0);
                    new Thread() {
                        public void run() {
                            try {
                                currentSavePath = f;
                                OutputStream ostream = new BufferedOutputStream
                                    (new FileOutputStream(f));
                                trans.writeImage
                                    (img, new TranscoderOutput(ostream));
                                ostream.close();
                            } catch (Exception ex) {}
                            statusBar.setMessage
                                (resources.getString("Message.done"));
                        }
                    }.start();
                }
            }
        }
    }

    /**
     * To view the source of the current document.
     */
    public class ViewSourceAction extends AbstractAction {
        public ViewSourceAction() {}
        public void actionPerformed(ActionEvent e) {
            if (svgDocument == null) {
                return;
            }

            final ParsedURL u = new ParsedURL(svgDocument.getURL());

            final JFrame fr = new JFrame(u.toString());
            fr.setSize(resources.getInteger("ViewSource.width"),
                       resources.getInteger("ViewSource.height"));
            final XMLTextEditor ta  = new XMLTextEditor();
            // ta.setLineWrap(true);
            ta.setFont(new Font("monospaced", Font.PLAIN, 12));

            JScrollPane scroll = new JScrollPane();
            scroll.getViewport().add(ta);
            scroll.setVerticalScrollBarPolicy
                (JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
            fr.getContentPane().add(scroll, BorderLayout.CENTER);

            new Thread() {
                public void run() {
                    char [] buffer = new char[4096];

                    try {
                        Document  doc = new XMLDocument();

                        ParsedURL purl = new ParsedURL(svgDocument.getURL());
                        InputStream is
                            = u.openStream(getInputHandler(purl).
                                           getHandledMimeTypes());
                        // u.openStream(MimeTypeConstants.MIME_TYPES_SVG_LIST.iterator());

                        Reader in = XMLUtilities.createXMLDocumentReader(is);
                        int len;
                        while ((len=in.read(buffer, 0, buffer.length)) != -1) {
                            doc.insertString(doc.getLength(),
                                             new String(buffer, 0, len), null);
                        }

                        ta.setDocument(doc);
                        ta.setEditable(false);
                        // ta.setBackground(Color.white);
                        fr.setVisible(true);
                    } catch (Exception ex) {
                        userAgent.displayError(ex);
                    }
                }
            }.start();
        }
    }

    /**
     * To flush image cache (purely for debugging purposes)
     */
    public class FlushAction extends AbstractAction {
        public FlushAction() {}
        public void actionPerformed(ActionEvent e) {
            svgCanvas.flush();
            // Force redraw...
            svgCanvas.setRenderingTransform(svgCanvas.getRenderingTransform());
        }
    }

    /**
     * To toggle visiblity of JavaScript Debugger.
     */
    public class ToggleDebuggerAction extends AbstractAction {
        public ToggleDebuggerAction() {
            super("Toggle Debugger Action");
        }

        public void actionPerformed(ActionEvent e) {
            if (debugger == null) {
                showDebugger();
            } else {
                hideDebugger();
            }
        }
    }

    /**
     * To go back to the previous transform
     */
    public class PreviousTransformAction extends    AbstractAction
                                         implements JComponentModifier {
        List components = new LinkedList();
        public PreviousTransformAction() {}
        public void actionPerformed(ActionEvent e) {
            if (transformHistory.canGoBack()) {
                transformHistory.back();
                update();
                nextTransformAction.update();
                svgCanvas.setRenderingTransform(transformHistory.currentTransform());
            }
        }

        public void addJComponent(JComponent c) {
            components.add(c);
            c.setEnabled(false);
        }

        protected void update() {
            boolean b = transformHistory.canGoBack();
            for (Object component : components) {
                ((JComponent) component).setEnabled(b);
            }
        }
    }

    /**
     * To go forward to the next transform
     */
    public class NextTransformAction extends    AbstractAction
                                         implements JComponentModifier {
        List components = new LinkedList();
        public NextTransformAction() {}
        public void actionPerformed(ActionEvent e) {
            if (transformHistory.canGoForward()) {
                transformHistory.forward();
                update();
                previousTransformAction.update();
                svgCanvas.setRenderingTransform(transformHistory.currentTransform());
            }
        }

        public void addJComponent(JComponent c) {
            components.add(c);
            c.setEnabled(false);
        }

        protected void update() {
            boolean b = transformHistory.canGoForward();
            for (Object component : components) {
                ((JComponent) component).setEnabled(b);
            }
        }
    }

    /**
     * To apply the selected author stylesheet
     */
    public class UseStylesheetAction extends    AbstractAction
                                     implements JComponentModifier {

        List components = new LinkedList();

        public UseStylesheetAction() {}

        public void actionPerformed(ActionEvent e) {
        }

        public void addJComponent(JComponent c) {
            components.add(c);
            c.setEnabled(false);
        }

        protected void update() {
            alternateStyleSheet = null;
            Iterator it = components.iterator();
            SVGDocument doc = svgCanvas.getSVGDocument();
            while (it.hasNext()) {
                JComponent stylesheetMenu = (JComponent)it.next();
                stylesheetMenu.removeAll();
                stylesheetMenu.setEnabled(false);

                ButtonGroup buttonGroup = new ButtonGroup();

                for (Node n = doc.getFirstChild();
                     n != null && n.getNodeType() != Node.ELEMENT_NODE;
                     n = n.getNextSibling()) {
                    if (n instanceof StyleSheetProcessingInstruction) {
                        StyleSheetProcessingInstruction sspi;
                        sspi = (StyleSheetProcessingInstruction)n;
                        HashMap<String, String> attrs = sspi.getPseudoAttributes();
                        final String title = attrs.get("title");
                        String alt = attrs.get("alternate");
                        if (title != null && "yes".equals(alt)) {
                            JRadioButtonMenuItem button;
                            button = new JRadioButtonMenuItem(title);

                            button.addActionListener
                                (new java.awt.event.ActionListener() {
                                    public void actionPerformed(ActionEvent e) {
                                        SVGOMDocument doc;
                                        doc = (SVGOMDocument)svgCanvas.getSVGDocument();
                                        doc.clearViewCSS();
                                        alternateStyleSheet = title;
                                        svgCanvas.setSVGDocument(doc);
                                    }
                                });

                            buttonGroup.add(button);
                            stylesheetMenu.add(button);
                            stylesheetMenu.setEnabled(true);
                        }
                    }
                }
            }
        }
    }

    /**
     * To restart after a pause.
     */
    public class PlayAction extends   AbstractAction
                            implements JComponentModifier {
        List components = new LinkedList();
        public PlayAction() {}
        public void actionPerformed(ActionEvent e) {
            svgCanvas.resumeProcessing();
        }

        public void addJComponent(JComponent c) {
            components.add(c);
            c.setEnabled(false);
        }

        public void update(boolean enabled) {
            for (Object component : components) {
                ((JComponent) component).setEnabled(enabled);
            }
        }
    }

    /**
     * To pause a document.
     */
    public class PauseAction extends   AbstractAction
                            implements JComponentModifier {
        List components = new LinkedList();
        public PauseAction() {}
        public void actionPerformed(ActionEvent e) {
            svgCanvas.suspendProcessing();
        }

        public void addJComponent(JComponent c) {
            components.add(c);
            c.setEnabled(false);
        }

        public void update(boolean enabled) {
            for (Object component : components) {
                ((JComponent) component).setEnabled(enabled);
            }
        }
    }

    /**
     * To stop the current processing.
     */
    public class StopAction extends    AbstractAction
                            implements JComponentModifier {
        List components = new LinkedList();
        public StopAction() {}
        public void actionPerformed(ActionEvent e) {
            svgCanvas.stopProcessing();
        }

        public void addJComponent(JComponent c) {
            components.add(c);
            c.setEnabled(false);
        }

        public void update(boolean enabled) {
            for (Object component : components) {
                ((JComponent) component).setEnabled(enabled);
            }
        }
    }

    /**
     * To show the set transform dialog
     */
    public class SetTransformAction extends AbstractAction {
        public SetTransformAction(){}
        public void actionPerformed(ActionEvent e){
            if (transformDialog == null){
                transformDialog
                    = JAffineTransformChooser.createDialog
                    (JSVGViewerFrame.this,
                     resources.getString("SetTransform.title"));
            }

            AffineTransform txf = transformDialog.showDialog();
            if(txf != null){
                AffineTransform at = svgCanvas.getRenderingTransform();
                if(at == null){
                    at = new AffineTransform();
                }

                txf.concatenate(at);
                svgCanvas.setRenderingTransform(txf);
            }
        }
    }

    /**
     * To display the memory monitor.
     */
    public class MonitorAction extends AbstractAction {
        public MonitorAction() {}
        public void actionPerformed(ActionEvent e) {
            if (memoryMonitorFrame == null) {
                memoryMonitorFrame = new MemoryMonitor();
                Rectangle fr = getBounds();
                Dimension md = memoryMonitorFrame.getSize();
                memoryMonitorFrame.setLocation(fr.x + (fr.width  - md.width) / 2,
                                               fr.y + (fr.height - md.height) / 2);
            }
            memoryMonitorFrame.setVisible(true);
        }
    }

    /**
     * To display the Find dialog
     */
    public class FindDialogAction extends AbstractAction {
        public FindDialogAction() {}
        public void actionPerformed(ActionEvent e) {
            if (findDialog == null) {
                findDialog = new FindDialog(JSVGViewerFrame.this, svgCanvas);
                findDialog.setGraphicsNode(svgCanvas.getGraphicsNode());
                findDialog.pack();
                Rectangle fr = getBounds();
                Dimension td = findDialog.getSize();
                findDialog.setLocation(fr.x + (fr.width  - td.width) / 2,
                                       fr.y + (fr.height - td.height) / 2);
            }
            findDialog.setVisible(true);
        }
    }

    /**
     * To display the Thumbnail dialog
     */
    public class ThumbnailDialogAction extends AbstractAction {
        public ThumbnailDialogAction() {}
        public void actionPerformed(ActionEvent e) {
            if (thumbnailDialog == null) {
                thumbnailDialog
                    = new ThumbnailDialog(JSVGViewerFrame.this, svgCanvas);
                thumbnailDialog.pack();
                Rectangle fr = getBounds();
                Dimension td = thumbnailDialog.getSize();
                thumbnailDialog.setLocation(fr.x + (fr.width  - td.width) / 2,
                                            fr.y + (fr.height - td.height) / 2);
            }
            thumbnailDialog.setInteractionEnabled
                (!svgCanvas.getDisableInteractions());
            thumbnailDialog.setVisible(true);
        }
    }

    /**
     * To display the document full screen
     */
    public class FullScreenAction extends AbstractAction {
        public FullScreenAction() {}

        public void actionPerformed(ActionEvent e) {
            if (window == null || !window.isVisible()) {
                if (window == null) {
                    window = new JWindow(JSVGViewerFrame.this);
                    Dimension size = Toolkit.getDefaultToolkit().getScreenSize();
                    window.setSize(size);
                }
                // Go to full screen in JWindow)
                svgCanvas.getParent().remove(svgCanvas);
                window.getContentPane().add(svgCanvas);
                window.setVisible(true);
                window.toFront();
                svgCanvas.requestFocus();
            } else {
                // Go back to JSVGViewerFrame display
                svgCanvas.getParent().remove(svgCanvas);
                svgCanvasPanel.add(svgCanvas, BorderLayout.CENTER);
                window.setVisible(false);
            }
        }
    }

    /**
     * To display the DOM viewer of the document
     */
    public class DOMViewerAction extends AbstractAction {

        public DOMViewerAction() {
        }

        public void actionPerformed(ActionEvent e) {
            openDOMViewer();
        }

        public void openDOMViewer() {
            if (domViewer == null || domViewer.isDisplayable()) {
                domViewer = new DOMViewer
                    (svgCanvas.new JSVGViewerDOMViewerController());
                Rectangle fr = getBounds();
                Dimension td = domViewer.getSize();
                domViewer.setLocation(fr.x + (fr.width - td.width) / 2,
                                      fr.y + (fr.height - td.height) / 2);
            }
            domViewer.setVisible(true);
        }

        public DOMViewer getDOMViewer() {
            return domViewer;
        }
    }

    // ActionMap /////////////////////////////////////////////////////

    /**
     * The map that contains the action listeners
     */
    protected Map listeners = new HashMap();

    /**
     * Returns the action associated with the given string
     * or null on error
     * @param key the key mapped with the action to get
     * @throws MissingListenerException if the action is not found
     */
    public Action getAction(String key) throws MissingListenerException {
        Action result = (Action)listeners.get(key);
        //if (result == null) {
        //result = canvas.getAction(key);
        //}
        if (result == null) {
            throw new MissingListenerException("Can't find action.", RESOURCES, key);
        }
        return result;
    }

    // SVGDocumentLoaderListener ///////////////////////////////////////////

    long time; // For debug.

    /**
     * Called when the loading of a document was started.
     */
    public void documentLoadingStarted(SVGDocumentLoaderEvent e) {
        String msg = resources.getString("Message.documentLoad");
        if (debug) {
            System.out.println(msg);
            time = System.currentTimeMillis();
        }
        statusBar.setMainMessage(msg);
        stopAction.update(true);
        svgCanvas.setCursor(WAIT_CURSOR);
    }


    /**
     * Called when the loading of a document was completed.
     */
    public void documentLoadingCompleted(SVGDocumentLoaderEvent e) {
        if (debug) {
            System.out.print(resources.getString("Message.documentLoadTime"));
            System.out.println((System.currentTimeMillis() - time) + " ms");
        }

        setSVGDocument(e.getSVGDocument(),
                       e.getSVGDocument().getURL(),
                       e.getSVGDocument().getTitle());
    }

    /**
     * Forces the viewer frame to show the input SVGDocument
     */
    public void setSVGDocument(SVGDocument svgDocument,
                               String svgDocumentURL,
                               String svgDocumentTitle) {
        this.svgDocument = svgDocument;

        if (domViewer != null) {
            if(domViewer.isVisible() && svgDocument != null) {
                domViewer.setDocument(svgDocument,
                                      (ViewCSS)svgDocument.getDocumentElement());
            } else {
                domViewer.dispose();
                domViewer = null;
            }
        }
        stopAction.update(false);
        svgCanvas.setCursor(DEFAULT_CURSOR);
        String s = svgDocumentURL;
        locationBar.setText(s);
        if (debugger != null) {
            debugger.detach();
            debugger.setDocumentURL(s);
        }
        if (title == null) {
            title = getTitle();
        }

        String dt = svgDocumentTitle;
        if (dt.length() != 0) {
            setTitle(title + ": " + dt);
        } else {
            int i = s.lastIndexOf("/");
            if (i == -1)
                i = s.lastIndexOf("\\");
            if (i == -1) {
                setTitle(title + ": " + s);
            } else {
                setTitle(title + ": " + s.substring(i + 1));
            }
        }

        localHistory.update(s);
        application.addVisitedURI(s);
        backAction.update();
        forwardAction.update();

        transformHistory = new TransformHistory();
        previousTransformAction.update();
        nextTransformAction.update();

        useStylesheetAction.update();
    }

    /**
     * Called when the loading of a document was cancelled.
     */
    public void documentLoadingCancelled(SVGDocumentLoaderEvent e) {
        String msg = resources.getString("Message.documentCancelled");
        if (debug) {
            System.out.println(msg);
        }
        statusBar.setMainMessage("");
        statusBar.setMessage(msg);
        stopAction.update(false);
        svgCanvas.setCursor(DEFAULT_CURSOR);
    }

    /**
     * Called when the loading of a document has failed.
     */
    public void documentLoadingFailed(SVGDocumentLoaderEvent e) {
        String msg = resources.getString("Message.documentFailed");
        if (debug) {
            System.out.println(msg);
        }
        statusBar.setMainMessage("");
        statusBar.setMessage(msg);
        stopAction.update(false);
        svgCanvas.setCursor(DEFAULT_CURSOR);
    }

    // GVTTreeBuilderListener //////////////////////////////////////////////

    /**
     * Called when a build started.
     * The data of the event is initialized to the old document.
     */
    public void gvtBuildStarted(GVTTreeBuilderEvent e) {
        String msg = resources.getString("Message.treeBuild");
        if (debug) {
            System.out.println(msg);
            time = System.currentTimeMillis();
        }
        statusBar.setMainMessage(msg);
        stopAction.update(true);
        svgCanvas.setCursor(WAIT_CURSOR);
    }

    /**
     * Called when a build was completed.
     */
    public void gvtBuildCompleted(GVTTreeBuilderEvent e) {
        if (debug) {
            System.out.print(resources.getString("Message.treeBuildTime"));
            System.out.println((System.currentTimeMillis() - time) + " ms");
        }
        if (findDialog != null) {
            if(findDialog.isVisible()) {
                findDialog.setGraphicsNode(svgCanvas.getGraphicsNode());
            } else {
                findDialog.dispose();
                findDialog = null;
            }
        }
        stopAction.update(false);
        svgCanvas.setCursor(DEFAULT_CURSOR);
        svgCanvas.setSelectionOverlayXORMode
            (application.isSelectionOverlayXORMode());
        svgCanvas.requestFocus();  // request focus when load completes.
        if (debugger != null) {
            debugger.attach();
        }
    }

    /**
     * Called when a build was cancelled.
     */
    public void gvtBuildCancelled(GVTTreeBuilderEvent e) {
        String msg = resources.getString("Message.treeCancelled");
        if (debug) {
            System.out.println(msg);
        }
        statusBar.setMainMessage("");
        statusBar.setMessage(msg);
        stopAction.update(false);
        svgCanvas.setCursor(DEFAULT_CURSOR);
        svgCanvas.setSelectionOverlayXORMode
            (application.isSelectionOverlayXORMode());
    }

    /**
     * Called when a build failed.
     */
    public void gvtBuildFailed(GVTTreeBuilderEvent e) {
        String msg = resources.getString("Message.treeFailed");
        if (debug) {
            System.out.println(msg);
        }
        statusBar.setMainMessage("");
        statusBar.setMessage(msg);
        stopAction.update(false);
        svgCanvas.setCursor(DEFAULT_CURSOR);
        svgCanvas.setSelectionOverlayXORMode
            (application.isSelectionOverlayXORMode());
        if (autoAdjust) {
            pack();
        }
    }

    // SVGLoadEventDispatcherListener //////////////////////////////////////

    /**
     * Called when a onload event dispatch started.
     */
    public void svgLoadEventDispatchStarted(SVGLoadEventDispatcherEvent e) {
        String msg = resources.getString("Message.onload");
        if (debug) {
            System.out.println(msg);
            time = System.currentTimeMillis();
        }
        stopAction.update(true);
        statusBar.setMainMessage(msg);
    }

    /**
     * Called when a onload event dispatch was completed.
     */
    public void svgLoadEventDispatchCompleted(SVGLoadEventDispatcherEvent e) {
        if (debug) {
            System.out.print(resources.getString("Message.onloadTime"));
            System.out.println((System.currentTimeMillis() - time) + " ms");
        }
        stopAction.update(false);
        statusBar.setMainMessage("");
        statusBar.setMessage(resources.getString("Message.done"));
    }

    /**
     * Called when a onload event dispatch was cancelled.
     */
    public void svgLoadEventDispatchCancelled(SVGLoadEventDispatcherEvent e) {
        String msg = resources.getString("Message.onloadCancelled");
        if (debug) {
            System.out.println(msg);
        }
        stopAction.update(false);
        statusBar.setMainMessage("");
        statusBar.setMessage(msg);
    }

    /**
     * Called when a onload event dispatch failed.
     */
    public void svgLoadEventDispatchFailed(SVGLoadEventDispatcherEvent e) {
        String msg = resources.getString("Message.onloadFailed");
        if (debug) {
            System.out.println(msg);
        }
        stopAction.update(false);
        statusBar.setMainMessage("");
        statusBar.setMessage(msg);
    }

    // GVTTreeRendererListener /////////////////////////////////////////////

    /**
     * Called when a rendering is in its preparing phase.
     */
    public void gvtRenderingPrepare(GVTTreeRendererEvent e) {
        if (debug) {
            String msg = resources.getString("Message.treeRenderingPrep");
            System.out.println(msg);
            time = System.currentTimeMillis();
        }
        stopAction.update(true);
        svgCanvas.setCursor(WAIT_CURSOR);
        statusBar.setMainMessage(resources.getString("Message.treeRendering"));
    }

    /**
     * Called when a rendering started.
     */
    public void gvtRenderingStarted(GVTTreeRendererEvent e) {
        if (debug) {
            String msg = resources.getString("Message.treeRenderingPrepTime");
            System.out.print(msg);
            System.out.println((System.currentTimeMillis() - time) + " ms");
            time = System.currentTimeMillis();
            msg = resources.getString("Message.treeRenderingStart");
            System.out.println(msg);
        }
        // Do nothing
    }

    /**
     * Called when a rendering was completed.
     */
    public void gvtRenderingCompleted(GVTTreeRendererEvent e) {
        if (debug) {
            String msg = resources.getString("Message.treeRenderingTime");
            System.out.print(msg);
            System.out.println((System.currentTimeMillis() - time) + " ms");
        }
        statusBar.setMainMessage("");
        statusBar.setMessage(resources.getString("Message.done"));
        if (!svgCanvas.isDynamic() || managerStopped) {
            stopAction.update(false);
        }
        svgCanvas.setCursor(DEFAULT_CURSOR);

        transformHistory.update(svgCanvas.getRenderingTransform());
        previousTransformAction.update();
        nextTransformAction.update();
    }

    /**
     * Called when a rendering was cancelled.
     */
    public void gvtRenderingCancelled(GVTTreeRendererEvent e) {
        String msg = resources.getString("Message.treeRenderingCancelled");
        if (debug) {
            System.out.println(msg);
        }
        statusBar.setMainMessage("");
        statusBar.setMessage(msg);
        if (!svgCanvas.isDynamic()) {
            stopAction.update(false);
        }
        svgCanvas.setCursor(DEFAULT_CURSOR);
    }

    /**
     * Called when a rendering failed.
     */
    public void gvtRenderingFailed(GVTTreeRendererEvent e) {
        String msg = resources.getString("Message.treeRenderingFailed");
        if (debug) {
            System.out.println(msg);
        }
        statusBar.setMainMessage("");
        statusBar.setMessage(msg);
        if (!svgCanvas.isDynamic()) {
            stopAction.update(false);
        }
        svgCanvas.setCursor(DEFAULT_CURSOR);
    }

    // LinkActivationListener /////////////////////////////////////////

    /**
     * Called when a link was activated.
     */
    public void linkActivated(LinkActivationEvent e) {
        String s = e.getReferencedURI();
        if (svgDocument != null) {
            ParsedURL docURL = new ParsedURL(svgDocument.getURL());
            ParsedURL url    = new ParsedURL(docURL, s);
            if (!url.sameFile(docURL)) {
                return;
            }

            if (s.indexOf( '#' ) != -1) {
                localHistory.update(s);
                locationBar.setText(s);
                if (debugger != null) {
                    debugger.detach();
                    debugger.setDocumentURL(s);
                }
                application.addVisitedURI(s);
                backAction.update();
                forwardAction.update();

                transformHistory = new TransformHistory();
                previousTransformAction.update();
                nextTransformAction.update();
            }
        }
    }

    // UpdateManagerListener ////////////////////////////////////////////////

    /**
     * Called when the manager was started.
     */
    public void managerStarted(UpdateManagerEvent e) {
        if (debug) {
            String msg = resources.getString("Message.updateManagerStarted");
            System.out.println(msg);
        }
        managerStopped = false;
        playAction.update(false);
        pauseAction.update(true);
        stopAction.update(true);
    }

    /**
     * Called when the manager was suspended.
     */
    public void managerSuspended(UpdateManagerEvent e) {
        if (debug) {
            String msg = resources.getString("Message.updateManagerSuspended");
            System.out.println(msg);
        }
        playAction.update(true);
        pauseAction.update(false);
    }

    /**
     * Called when the manager was resumed.
     */
    public void managerResumed(UpdateManagerEvent e) {
        if (debug) {
            String msg = resources.getString("Message.updateManagerResumed");
            System.out.println(msg);
        }
        playAction.update(false);
        pauseAction.update(true);
    }

    /**
     * Called when the manager was stopped.
     */
    public void managerStopped(UpdateManagerEvent e) {
        if (debug) {
            String msg = resources.getString("Message.updateManagerStopped");
            System.out.println(msg);
        }
        managerStopped = true;
        playAction.update(false);
        pauseAction.update(false);
        stopAction.update(false);
    }

    /**
     * Called when an update started.
     */
    public void updateStarted(final UpdateManagerEvent e) {
    }

    /**
     * Called when an update was completed.
     */
    public void updateCompleted(final UpdateManagerEvent e) {
    }

    /**
     * Called when an update failed.
     */
    public void updateFailed(UpdateManagerEvent e) {
    }

    /**
     * This class implements a SVG user agent.
     */
    protected class UserAgent implements SVGUserAgent {

        private float sourceResolution = 96;

        /**
         * Creates a new SVGUserAgent.
         */
        protected UserAgent() {
        }

        /**
         * Displays an error message.
         */
        public void displayError(String message) {
            if (debug) {
                System.err.println(message);
            }
            JOptionPane pane =
                new JOptionPane(message, JOptionPane.ERROR_MESSAGE);
            JDialog dialog = pane.createDialog(JSVGViewerFrame.this, "ERROR");
            dialog.setModal(false);
            dialog.setVisible(true);
        }

        /**
         * Displays an error resulting from the specified Exception.
         */
        public void displayError(Exception ex) {
            if (debug) {
                ex.printStackTrace();
            }
            JErrorPane pane = new JErrorPane(ex, JOptionPane.ERROR_MESSAGE);
            JDialog dialog = pane.createDialog(JSVGViewerFrame.this, "ERROR");
            dialog.setModal(false);
            dialog.setVisible(true);
        }

        /**
         * Displays a message in the User Agent interface.
         * The given message is typically displayed in a status bar.
         */
        public void displayMessage(String message) {
            statusBar.setMessage(message);
        }

        /**
         * Shows an alert dialog box.
         */
        public void showAlert(String message) {
            svgCanvas.showAlert(message);
        }

        /**
         * Shows a prompt dialog box.
         */
        public String showPrompt(String message) {
            return svgCanvas.showPrompt(message);
        }

        /**
         * Shows a prompt dialog box.
         */
        public String showPrompt(String message, String defaultValue) {
            return svgCanvas.showPrompt(message, defaultValue);
        }

        /**
         * Shows a confirm dialog box.
         */
        public boolean showConfirm(String message) {
            return svgCanvas.showConfirm(message);
        }

        /**
         * Returns the size of a px CSS unit in millimeters.
         */
        public float getPixelUnitToMillimeter() {
            return UnitConv.IN2MM / getSourceResolution();
        }

        /**
         * Returns the size of a px CSS unit in millimeters.
         * This will be removed after next release.
         * @see #getPixelUnitToMillimeter()
         */
        public float getPixelToMM() {
            return getPixelUnitToMillimeter();

        }

        /**
         * Returns the default font family.
         */
        public String getDefaultFontFamily() {
            return application.getDefaultFontFamily();
        }

        /**
         * Returns the  medium font size.
         */
        public float getMediumFontSize() {
            // 9pt (72pt == 1in)
            return 9f * UnitConv.IN2MM / (UnitConv.IN2PT * getPixelUnitToMillimeter());
        }

        /**
         * Returns a lighter font-weight.
         */
        public float getLighterFontWeight(float f) {
            // Round f to nearest 100...
            int weight = ((int)((f+50)/100))*100;
            switch (weight) {
            case 100: return 100;
            case 200: return 100;
            case 300: return 200;
            case 400: return 300;
            case 500: return 400;
            case 600: return 400;
            case 700: return 400;
            case 800: return 400;
            case 900: return 400;
            default:
                throw new IllegalArgumentException("Bad Font Weight: " + f);
            }
        }

        /**
         * Returns a bolder font-weight.
         */
        public float getBolderFontWeight(float f) {
            // Round f to nearest 100...
            int weight = ((int)((f+50)/100))*100;
            switch (weight) {
            case 100: return 600;
            case 200: return 600;
            case 300: return 600;
            case 400: return 600;
            case 500: return 600;
            case 600: return 700;
            case 700: return 800;
            case 800: return 900;
            case 900: return 900;
            default:
                throw new IllegalArgumentException("Bad Font Weight: " + f);
            }
        }


        /**
         * Returns the language settings.
         */
        public String getLanguages() {
            return application.getLanguages();
        }

        /**
         * Returns the user stylesheet uri.
         * @return null if no user style sheet was specified.
         */
        public String getUserStyleSheetURI() {
            return application.getUserStyleSheetURI();
        }

        /**
         * Returns the class name of the XML parser.
         */
        public String getXMLParserClassName() {
            return application.getXMLParserClassName();
        }

        /**
         * Returns true if the XML parser must be in validation mode, false
         * otherwise.
         */
        public boolean isXMLParserValidating() {
            return application.isXMLParserValidating();
        }

        /**
         * Returns this user agent's CSS media.
         */
        public String getMedia() {
            return application.getMedia();
        }

        /**
         * Returns this user agent's alternate style-sheet title.
         */
        public String getAlternateStyleSheet() {
            return alternateStyleSheet;
        }

        /**
         * Opens a link.
         * @param uri The document URI.
         * @param newc Whether the link should be activated in a new component.
         */
        public void openLink(String uri, boolean newc) {
            if (newc) {
                application.openLink(uri);
            } else {
                showSVGDocument(uri);
            }
        }

        /**
         * Tells whether the given extension is supported by this
         * user agent.
         */
        public boolean supportExtension(String s) {
            return false;
        }

        public void handleElement(Element elt, Object data){
        }

        /**
         * Returns the security settings for the given script
         * type, script url and document url
         *
         * @param scriptType type of script, as found in the
         *        type attribute of the &lt;script&gt; element.
         * @param scriptURL url for the script, as defined in
         *        the script's xlink:href attribute. If that
         *        attribute was empty, then this parameter should
         *        be null
         * @param docURL url for the document into which the
         *        script was found.
         */
        public ScriptSecurity getScriptSecurity(String scriptType,
                                                ParsedURL scriptURL,
                                                ParsedURL docURL){
            if (!application.canLoadScriptType(scriptType)) {
                return new NoLoadScriptSecurity(scriptType);
            } else {
                switch(application.getAllowedScriptOrigin()) {
                case ResourceOrigin.ANY:
                    return new RelaxedScriptSecurity(scriptType,
                                                     scriptURL,
                                                     docURL);
                case ResourceOrigin.DOCUMENT:
                    return new DefaultScriptSecurity(scriptType,
                                                     scriptURL,
                                                     docURL);
                case ResourceOrigin.EMBEDED:
                    return new EmbededScriptSecurity(scriptType,
                                                     scriptURL,
                                                     docURL);
                default:
                    return new NoLoadScriptSecurity(scriptType);
                }
            }
        }

        /**
         * This method throws a SecurityException if the script
         * of given type, found at url and referenced from docURL
         * should not be loaded.
         *
         * This is a convenience method to call checkLoadScript
         * on the ScriptSecurity strategy returned by
         * getScriptSecurity.
         *
         * @param scriptType type of script, as found in the
         *        type attribute of the &lt;script&gt; element.
         * @param scriptURL url for the script, as defined in
         *        the script's xlink:href attribute. If that
         *        attribute was empty, then this parameter should
         *        be null
         * @param docURL url for the document into which the
         *        script was found.
         */
        public void checkLoadScript(String scriptType,
                                    ParsedURL scriptURL,
                                    ParsedURL docURL) throws SecurityException {
            ScriptSecurity s = getScriptSecurity(scriptType,
                                                     scriptURL,
                                                     docURL);

            if (s != null) {
                s.checkLoadScript();
            }
        }

        /**
         * Returns the security settings for the given
         * resource url and document url
         *
         * @param resourceURL url for the resource, as defined in
         *        the resource's xlink:href attribute. If that
         *        attribute was empty, then this parameter should
         *        be null
         * @param docURL url for the document into which the
         *        resource was found.
         */
        public ExternalResourceSecurity
            getExternalResourceSecurity(ParsedURL resourceURL,
                                        ParsedURL docURL){
            switch(application.getAllowedExternalResourceOrigin()) {
            case ResourceOrigin.ANY:
                return new RelaxedExternalResourceSecurity(resourceURL,
                                                           docURL);
            case ResourceOrigin.DOCUMENT:
                return new DefaultExternalResourceSecurity(resourceURL,
                                                           docURL);
            case ResourceOrigin.EMBEDED:
                return new EmbededExternalResourceSecurity(resourceURL);
            default:
                return new NoLoadExternalResourceSecurity();
            }
        }

        /**
         * This method throws a SecurityException if the resource
         * found at url and referenced from docURL
         * should not be loaded.
         *
         * This is a convenience method to call checkLoadExternalResource
         * on the ExternalResourceSecurity strategy returned by
         * getExternalResourceSecurity.
         *
         * @param resourceURL url for the script, as defined in
         *        the resource's xlink:href attribute. If that
         *        attribute was empty, then this parameter should
         *        be null
         * @param docURL url for the document into which the
         *        resource was found.
         */
        public void
            checkLoadExternalResource(ParsedURL resourceURL,
                                      ParsedURL docURL) throws SecurityException {
            ExternalResourceSecurity s
                =  getExternalResourceSecurity(resourceURL, docURL);

            if (s != null) {
                s.checkLoadExternalResource();
            }
        }

        @Override
        public float getSourceResolution() {
            return sourceResolution;
        }

        @Override
        public void setSourceResolution(float sourceResolution) {
            this.sourceResolution = sourceResolution;
        }
    }

    /**
     * A FileFilter used when exporting the SVG document as an image.
     */
    protected static class ImageFileFilter extends FileFilter {

        /** The extension of the image filename. */
        protected String extension;

        public ImageFileFilter(String extension) {
            this.extension = extension;
        }

        /**
         * Returns true if <code>f</code> is a file with the correct extension,
         * false otherwise.
         */
        public boolean accept(File f) {
            boolean accept = false;
            String fileName = null;
            if (f != null) {
                if (f.isDirectory()) {
                    accept = true;
                } else {
                    fileName = f.getPath().toLowerCase();
                    if (fileName.endsWith(extension)) {
                        accept = true;
                    }
                }
            }
            return accept;
        }

        /**
         * Returns the file description
         */
        public String getDescription() {
            return extension;
        }
    }
}
