/*
 * {{{ header & license
 * Copyright (c) 2004, 2005 Patrick Wright
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 * }}}
 */
package org.xhtmlrenderer.simple;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.KeyStroke;

/**
 * <p><code>FSScrollPane</code> is a JScrollPane set up to support keyboard navigation of an XHTML/XML
 * document rendered with Flying Saucer. In particular, it assigns key bindings to the view's {@link InputMap} 
 * for page-up, page-down, line-up/down, page-start and page-end.
 * The amount the document scrolls is based on the current viewport and the current line height.
 * If the view is resized, the scroll increment is automatically adjusted. Using FSScrollPane 
 * to display an {@link XHTMLPanel} should save you time as your users
 * will have standard keyboard navigation out of the box.</p>
 * 
 * <p>To use <code>FSScrollPane</code>, just instantiate it and add your XHTMLPanel on instantiation:
 * <pre>
 * XHTMLPanel panel = new XHTMLPanel();
 * FSScrollPane scroll = new FSScrollPane(view);
 * </pre>
 * <p>The current input mappings to keys are:
 * <dl>
 * <dt>Scroll to Start<dt><dd>CONTROL-HOME or HOME</dd>
 * <dt>Scroll Up 1 Page<dt><dd>PAGEUP</dd>
 * <dt>Scroll Up 1 Line<dt><dd>UP-ARROW</dd>
 * <dt>Scroll to Bottom<dt><dd>CONTROL-END or END</dd>
 * <dt>Scroll Down 1 Page<dt><dd>PAGEDOWN</dd>
 * <dt>Scroll Down 1 Line<dt><dd>DOWN-ARROW</dd>
 * </dl>
 * This class declares six constant strings you can use if you want to override one of these default
 * settings on the <code>InputMap</code>; these Strings will be trigger the relevant <code>Action</code>
 * associated with the scrolling. To change the key binding for "Scroll to Top" to <code>Alt-Home</code>,
 * do this:
 * <pre>
 * panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).
                put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, KeyEvent.ALT_MASK), FSScrollPane.PAGE_START);
 * </pre>
 *
 *
 * @author Patrick Wright
 */
public class FSScrollPane extends JScrollPane {
    private static final long serialVersionUID = 2L;

    /** Constant used for mapping a key binding to "scroll down 1 page" */
    public static final String PAGE_DOWN = "page-down";

    /** Constant used for mapping a key binding to "scroll up 1 page" */
    public static final String PAGE_UP = "page-up";

    /** Constant used for mapping a key binding to "scroll down 1 line" */
    public static final String LINE_DOWN = "down";

    /** Constant used for mapping a key binding to "scroll up 1 line" */
    public static final String LINE_UP = "up";

    /** Constant used for mapping a key binding to "scroll to end of document" */
    public static final String PAGE_END = "page-end";

    /** Constant used for mapping a key binding to "scroll to top of document" */
    public static final String PAGE_START = "page-start";
    

    public FSScrollPane() {
        this(null);
    }
        

    /** Instantiates a new FSScrollPane around the given Panel; see class documentation. */
    public FSScrollPane(JPanel aview) {
        super(aview, VERTICAL_SCROLLBAR_ALWAYS, HORIZONTAL_SCROLLBAR_ALWAYS);

        // TODO: need to get line-height, I think; this should not be fixed (PWW 28-01-05)
        getVerticalScrollBar().setUnitIncrement(15);
    }
    
    /* @Override */
    public void setViewportView(Component view)
    {
        setPreferredSize(new Dimension((int)view.getSize().getWidth(), (int)view.getSize().getHeight()));
        if (view instanceof JComponent) {
            setDefaultInputMap((JComponent) view);
            setDefaultActionMap((JComponent) view);
        }
        addResizeListener(view);
        super.setViewportView(view);
    }
    
    /** Assigns the default keyboard bindings on the view for document navigation. */
    private void setDefaultInputMap(JComponent view) {
        view.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).
                put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0), PAGE_DOWN);
        view.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).
                put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), PAGE_UP);
        view.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).
                put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), LINE_DOWN);
        view.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).
                put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), LINE_UP);
        view.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).
                put(KeyStroke.getKeyStroke(KeyEvent.VK_END, KeyEvent.CTRL_MASK), PAGE_END);
        view.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).
                put(KeyStroke.getKeyStroke(KeyEvent.VK_END, 0), PAGE_END);
        view.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).
                put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, KeyEvent.CTRL_MASK), PAGE_START);
        view.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).
                put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, 0), PAGE_START);
        
    }
    
    /** Assigns the default Actions for document navigation on the view. */
    private void setDefaultActionMap(JComponent view) {
        view.getActionMap().put(PAGE_DOWN,
                new AbstractAction() {
                    private static final long serialVersionUID = 1L;

                    public void actionPerformed(ActionEvent evt) {
                        JScrollBar sb = getVerticalScrollBar();
                        sb.getModel().setValue(sb.getModel().getValue() + sb.getBlockIncrement(1));
                    }
                });
        view.getActionMap().put(PAGE_END,
                new AbstractAction() {
                    private static final long serialVersionUID = 1L;

                    public void actionPerformed(ActionEvent evt) {
                        JScrollBar sb = getVerticalScrollBar();
                        sb.getModel().setValue(sb.getModel().getMaximum());
                    }
                });
        view.getActionMap().put(PAGE_UP,
                new AbstractAction() {
                    private static final long serialVersionUID = 1L;

                    public void actionPerformed(ActionEvent evt) {
                        JScrollBar sb = getVerticalScrollBar();
                        sb.getModel().setValue(sb.getModel().getValue() - sb.getBlockIncrement(-1));
                    }
                });
        view.getActionMap().put(PAGE_START,
                new AbstractAction() {
                    private static final long serialVersionUID = 1L;

                    public void actionPerformed(ActionEvent evt) {
                        JScrollBar sb = getVerticalScrollBar();
                        sb.getModel().setValue(0);
                    }
                });
        view.getActionMap().put(LINE_DOWN,
                new AbstractAction() {
                    private static final long serialVersionUID = 1L;

                    public void actionPerformed(ActionEvent evt) {
                        JScrollBar sb = getVerticalScrollBar();
                        sb.getModel().setValue(sb.getModel().getValue() + sb.getUnitIncrement(1));
                    }
                });
        view.getActionMap().put(LINE_UP,
                new AbstractAction() {
                    private static final long serialVersionUID = 1L;

                    public void actionPerformed(ActionEvent evt) {
                        JScrollBar sb = getVerticalScrollBar();
                        sb.getModel().setValue(sb.getModel().getValue() - sb.getUnitIncrement(-1));
                    }
                });
    }
    
    /** Adds a component listener on the view for resize events, to adjust the scroll increment. */
    private void addResizeListener(Component view) {
        view.addComponentListener( new ComponentAdapter() {
            /** Invoked when the component's size changes. Reset scrollable increment, because
             * page-down/up is relative to current view size.
             */
            public void componentResized(ComponentEvent e) {
                JScrollBar bar = getVerticalScrollBar();
                
                // NOTE: use the scroll pane size--the XHTMLPanel size is a virtual size of the entire
                // page
                
                // want to page down leaving the current line at the bottom be the first at the top
                // TODO: this will only work once unit increment is set correctly; multiplier is a workaround (PWW 28-01-05)
                int incr = (int)(getSize().getHeight() - (bar.getUnitIncrement(1) * 3));
                getVerticalScrollBar().setBlockIncrement(incr);
            }
        });        
    }
}
