/*****************************************************************************
 * Copyright (C) The Apache Software Foundation. All rights reserved.        *
 * ------------------------------------------------------------------------- *
 * This software is published under the terms of the Apache Software License *
 * version 1.1, a copy of which has been included with this distribution in  *
 * the LICENSE file.                                                         *
 *****************************************************************************/

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.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.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.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
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.JMenuBar;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JToolBar;
import javax.swing.JWindow;
import javax.swing.KeyStroke;
import javax.swing.filechooser.FileFilter;
import javax.swing.text.Document;
import javax.swing.text.PlainDocument;

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.UpdateManagerEvent;
import org.apache.batik.bridge.UpdateManagerListener;
import org.apache.batik.dom.StyleSheetProcessingInstruction;
import org.apache.batik.dom.svg.SVGOMDocument;
import org.apache.batik.dom.util.HashTable;
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.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.util.ParsedURL;
import org.apache.batik.util.Service;
import org.apache.batik.util.gui.DOMViewer;
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.ResourceManager;
import org.apache.batik.util.gui.resource.ToolBarFactory;
import org.apache.batik.xml.XMLUtilities;
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: JSVGViewerFrame.java,v 1.94 2003/06/17 01:37:56 deweese Exp $
 */
public class JSVGViewerFrame
    extends    JFrame
    implements ActionMap,
               SVGDocumentLoaderListener,
               GVTTreeBuilderListener,
               SVGLoadEventDispatcherListener,
               GVTTreeRendererListener,
               LinkActivationListener,
               UpdateManagerListener {

    /**
     * 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 {
        Class cl = null;
        try {
            cl = Class.forName(JDK_1_4_PRESENCE_TEST_CLASS);
        } catch (ClassNotFoundException e){
        }

        if (cl != null) {
            priorJDK1_4 = false;
        }
    }

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

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

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

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

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

    /**
     * Name for the os.name default
     */
    public final static 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 final static String PROPERTY_OS_WINDOWS_PREFIX 
        = Resources.getString("JSVGViewerFrame.property.os.windows.prefix");

    /**
     * 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 JSVGCanvas svgCanvas;

    /**
     * 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 currentExportPath = 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;

    /**
     * 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 JSVGCanvas(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 (JSVGViewerFrame.this.autoAdjust) {
                        JSVGViewerFrame.this.pack();
                    }
                }
            };
        
        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(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_MASK);
        imap.put(key, previousTransformAction);

        listeners.put(NEXT_TRANSFORM_ACTION, nextTransformAction);
        key = KeyStroke.getKeyStroke(KeyEvent.VK_L, KeyEvent.CTRL_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());

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

            localHistory = new LocalHistory(mb, this);

            String uri[] = application.getVisitedURIs();
            for (int i=0; i<uri.length; i++) {
                if (uri[i] != null && !"".equals(uri[i])) {
                    localHistory.update(uri[i]);
                }
            }
            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);

        } 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("")) {
                    try{
                        File f = new File(st);
                        if (f.exists()) {
                            if (f.isDirectory()) {
                                st = null;
                            } 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.
                    }

                    if (st != null) {
                        if (svgDocument != null) {
                            SVGOMDocument doc = (SVGOMDocument)svgDocument;
                            ParsedURL docPURL 
                                = new ParsedURL(svgDocument.getURL());
                            ParsedURL purl = new ParsedURL(docPURL, st);
                            String fi = svgCanvas.getFragmentIdentifier();
                            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);
                    }
                }
            }
        });

    }

    /**
     * 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;
    }

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

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

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

        public OpenAction() {
        }
        public void actionPerformed(ActionEvent e) {
            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
            //
            Iterator iter = getHandlers().iterator();
            while (iter.hasNext()) {
                SquiggleInputHandler handler 
                    = (SquiggleInputHandler)iter.next();
                fileChooser.addChoosableFileFilter
                    (new SquiggleInputHandlerFilter(handler));
            }
            
            int choice = fileChooser.showOpenDialog(JSVGViewerFrame.this);
            if (choice == JFileChooser.APPROVE_OPTION) {
                File f = fileChooser.getSelectedFile();
                
                currentPath = f;
                try { 
                    String furl = f.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();
                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();
            Iterator it = components.iterator();
            while (it.hasNext()) {
                ((JComponent)it.next()).setEnabled(b);
            }
        }
    }

    /**
     * To go forward to the previous 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();
            Iterator it = components.iterator();
            while (it.hasNext()) {
                ((JComponent)it.next()).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
                        //
                        pt.addTranscodingHint(pt.KEY_XML_PARSER_CLASSNAME,
                                              application.getXMLParserClassName());

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


                        pt.addTranscodingHint(pt.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 JPG.
     */
    public class ExportAsJPGAction extends AbstractAction {
        public ExportAsJPGAction() {}
        public void actionPerformed(ActionEvent e) {
            JFileChooser fileChooser =
                new JFileChooser(makeAbsolute(currentExportPath));
            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();
                    trans.addTranscodingHint
                        (JPEGTranscoder.KEY_XML_PARSER_CLASSNAME,
                         application.getXMLParserClassName());
                    trans.addTranscodingHint
                        (JPEGTranscoder.KEY_QUALITY, new Float(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 {
                                currentExportPath = f;
                                OutputStream ostream =
                                    new BufferedOutputStream(new FileOutputStream(f));
                                trans.writeImage(img, new TranscoderOutput(ostream));
                                ostream.flush();
                                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(currentExportPath));
            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) {

		// Start: By Jun Inamori (jun@oop-reserch.com)
                boolean isIndexed =
                    PNGOptionPanel.showDialog(JSVGViewerFrame.this);
		// End: By Jun Inamori (jun@oop-reserch.com)

                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();
                    trans.addTranscodingHint(PNGTranscoder.KEY_XML_PARSER_CLASSNAME,
                                             application.getXMLParserClassName());
                    trans.addTranscodingHint(PNGTranscoder.KEY_FORCE_TRANSPARENT_WHITE,
                                             new Boolean(true));

		    // Start: By Jun Inamori
		    if(isIndexed){
			trans.addTranscodingHint
                            (PNGTranscoder.KEY_INDEXED,new Integer(256));
		    }
		    // End: By Jun Inamori

                    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 {
                                currentExportPath = f;
                                OutputStream ostream =
                                    new BufferedOutputStream(new FileOutputStream(f));
                                trans.writeImage(img,
                                                 new TranscoderOutput(ostream));
                                ostream.flush();
                            } 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(currentExportPath));
            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();
                    trans.addTranscodingHint
                        (TIFFTranscoder.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 {
                                currentExportPath = f;
                                OutputStream ostream = new BufferedOutputStream
                                    (new FileOutputStream(f));
                                trans.writeImage
                                    (img, new TranscoderOutput(ostream));
                                ostream.flush();
                            } 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 JTextArea ta  = new JTextArea();
            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 PlainDocument();

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

                        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.show();
                    } 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 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();
            Iterator it = components.iterator();
            while (it.hasNext()) {
                ((JComponent)it.next()).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();
            Iterator it = components.iterator();
            while (it.hasNext()) {
                ((JComponent)it.next()).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() != n.ELEMENT_NODE;
                     n = n.getNextSibling()) {
                    if (n instanceof StyleSheetProcessingInstruction) {
                        StyleSheetProcessingInstruction sspi;
                        sspi = (StyleSheetProcessingInstruction)n;
                        HashTable attrs = sspi.getPseudoAttributes();
                        final String title = (String)attrs.get("title");
                        String alt = (String)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 {
        java.util.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) {
            Iterator it = components.iterator();
            while (it.hasNext()) {
                ((JComponent)it.next()).setEnabled(enabled);
            }
        }
    }

    /**
     * To pause a document.
     */
    public class PauseAction extends   AbstractAction
                            implements JComponentModifier {
        java.util.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) {
            Iterator it = components.iterator();
            while (it.hasNext()) {
                ((JComponent)it.next()).setEnabled(enabled);
            }
        }
    }

    /**
     * To stop the current processing.
     */
    public class StopAction extends    AbstractAction
                            implements JComponentModifier {
        java.util.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) {
            Iterator it = components.iterator();
            while (it.hasNext()) {
                ((JComponent)it.next()).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.show();
        }
    }

    /**
     * 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.show();
        }
    }

    /**
     * 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.show();
        }
    }

    /**
     * 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) {
            if (domViewer == null) {
                domViewer = new DOMViewer();
                if (svgDocument != null) {
                    domViewer.setDocument(svgDocument,
                                          (ViewCSS)svgDocument.getDocumentElement());
                }
                Rectangle fr = getBounds();
                Dimension td = domViewer.getSize();
                domViewer.setLocation(fr.x + (fr.width  - td.width) / 2,
                                      fr.y + (fr.height - td.height) / 2);
            }
            domViewer.show();
        }
    }

    // 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) {
        if (debug) {
            System.out.println("Document load started...");
            time = System.currentTimeMillis();
        }
        statusBar.setMainMessage(resources.getString("Message.documentLoad"));
        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("Document load completed in ");
            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;
        String t = svgCanvas.getFragmentIdentifier();
        if (t != null) {
            s += "#" + t;
        }

        locationBar.setText(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));
                }
            } 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) {
        if (debug) {
            System.out.println("Document load cancelled");
        }
        statusBar.setMainMessage("");
        statusBar.setMessage(resources.getString("Message.documentCancelled"));
        stopAction.update(false);
        svgCanvas.setCursor(DEFAULT_CURSOR);
    }

    /**
     * Called when the loading of a document has failed.
     */
    public void documentLoadingFailed(SVGDocumentLoaderEvent e) {
        if (debug) {
            System.out.println("Document load failed");
        }
        statusBar.setMainMessage("");
        statusBar.setMessage(resources.getString("Message.documentFailed"));
        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) {
        if (debug) {
            System.out.println("GVT build started...");
            time = System.currentTimeMillis();
        }
        statusBar.setMainMessage(resources.getString("Message.treeBuild"));
        stopAction.update(true);
        svgCanvas.setCursor(WAIT_CURSOR);
    }

    /**
     * Called when a build was completed.
     */
    public void gvtBuildCompleted(GVTTreeBuilderEvent e) {
        if (debug) {
            System.out.print("GVT build completed in ");
            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.
    }

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

    /**
     * Called when a build failed.
     */
    public void gvtBuildFailed(GVTTreeBuilderEvent e) {
        if (debug) {
            System.out.println("GVT build failed");
        }
        statusBar.setMainMessage("");
        statusBar.setMessage(resources.getString("Message.treeFailed"));
        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) {
        if (debug) {
            System.out.println("Onload dispatch started...");
            time = System.currentTimeMillis();
        }
        stopAction.update(true);
        statusBar.setMainMessage(resources.getString("Message.onload"));
    }

    /**
     * Called when a onload event dispatch was completed.
     */
    public void svgLoadEventDispatchCompleted(SVGLoadEventDispatcherEvent e) {
        if (debug) {
            System.out.print("Onload dispatch completed in ");
            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) {
        if (debug) {
            System.out.println("Onload dispatch cancelled.");
        }
        stopAction.update(false);
        statusBar.setMainMessage("");
        statusBar.setMessage(resources.getString("Message.onloadCancelled"));
    }

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

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

    /**
     * Called when a rendering is in its preparing phase.
     */
    public void gvtRenderingPrepare(GVTTreeRendererEvent e) {
        if (debug) {
            System.out.println("GVT rendering preparation...");
            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) {
            System.out.print("GVT rendering prepared in ");
            System.out.println((System.currentTimeMillis() - time) + " ms");
            time = System.currentTimeMillis();
            System.out.println("GVT rendering started...");
        }
        // Do nothing
    }

    /**
     * Called when a rendering was completed.
     */
    public void gvtRenderingCompleted(GVTTreeRendererEvent e) {
        if (debug) {
            System.out.print("GVT rendering completed in ");
            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) {
        if (debug) {
            System.out.println("GVT rendering cancelled");
        }
        statusBar.setMainMessage("");
        if (!svgCanvas.isDynamic()) {
            stopAction.update(false);
        }
        svgCanvas.setCursor(DEFAULT_CURSOR);
    }

    /**
     * Called when a rendering failed.
     */
    public void gvtRenderingFailed(GVTTreeRendererEvent e) {
        if (debug) {
            System.out.println("GVT rendering failed");
        }
        statusBar.setMainMessage("");
        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);
                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) {
            System.out.println("Update manager started...");
        }
        managerStopped = false;
        playAction.update(false);
        pauseAction.update(true);
        stopAction.update(true);
    }

    /**
     * Called when the manager was suspended.
     */
    public void managerSuspended(UpdateManagerEvent e) {
        if (debug) {
            System.out.println("Update manager suspended");
        }
        playAction.update(true);
        pauseAction.update(false);
    }

    /**
     * Called when the manager was resumed.
     */
    public void managerResumed(UpdateManagerEvent e) {
        if (debug) {
            System.out.println("Update manager resumed");
        }
        playAction.update(false);
        pauseAction.update(true);
    }

    /**
     * Called when the manager was stopped.
     */
    public void managerStopped(UpdateManagerEvent e) {
        if (debug) {
            System.out.println("Update Manager Stopped");
        }
        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 {

        /**
         * 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.show();
        }

        /**
         * 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.show();
        }

        /**
         * 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 0.26458333333333333333333333333333f; // 96dpi
        }
        
        /**
         * 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 * 25.4f / (72f * 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 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 
            checkLoadExternalResource(ParsedURL resourceURL,
                                      ParsedURL docURL) throws SecurityException {
            ExternalResourceSecurity s 
                =  getExternalResourceSecurity(resourceURL, docURL);
            
            if (s != null) {
                s.checkLoadExternalResource();
            }
        }
    }

    /**
     * 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 <tt>f</tt> 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;
        }
    }

}
