/*
 * This file is part of muCommander, http://www.mucommander.com
 *
 * muCommander is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * muCommander 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */


package com.mucommander.commons.util.ui.dialog;

import java.awt.Component;
import java.awt.Dialog;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;

import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.WindowConstants;
import javax.swing.border.EmptyBorder;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.mucommander.commons.runtime.OsFamily;
import com.mucommander.commons.util.ui.helper.FocusRequester;


/**
 * FocusDialog is a modal dialog which extends JDialog to provide the following additional functionalities :
 * <ul>
 *   <li>focus can be requested on a specified JComponent once the dialog has been made visible</li>
 *   <li>the screen location of the window can be set relatively to a Component specified in the constructor</li>
 *   <li>a minimum and/or maximum size can be specified and will be used by {@link #pack()} to calculate the effective dialog size</li>
 *   <li>by default, the 'Escape' key disposes the dialog, this can be disabled using {@link #setKeyboardDisposalEnabled(boolean)}</li>
 * </ul>
 * @author Maxence Bernard
 */
public class FocusDialog extends JDialog implements WindowListener {
    private static final Logger LOGGER = LoggerFactory.getLogger(FocusDialog.class);

    /** Minimum dimensions of this dialog, may be null */
    private Dimension minimumDimension;

    /** Maximum dimensions of this dialog, may be null */
    private Dimension maximumDimension;

    /** Has this window been activated yet ? */
    private boolean firstTimeActivated;

    /** The component that will receive the focus when this window is activated for the first time, may be null */
    private JComponent initialFocusComponent;

    private Component locationRelativeComp;

    private boolean keyboardDisposalEnabled = true;

    private final static String CUSTOM_DISPOSE_EVENT = "CUSTOM_DISPOSE_EVENT";

    
    public FocusDialog(Frame owner, String title, Component locationRelativeComp) {
        super(owner, title, true);
        init(locationRelativeComp);
    }

    public FocusDialog(Dialog owner, String title, Component locationRelativeComp) {
        super(owner, title, true);
        init(locationRelativeComp);
    }

    private void init(Component locationRelativeComp) {
        this.locationRelativeComp = locationRelativeComp;
        setLocationRelativeTo(locationRelativeComp);

        JPanel contentPane = (JPanel)getContentPane();
        contentPane.setBorder(new EmptyBorder(6, 8, 6, 8));
        setResizable(true);

        // Important: dispose (release resources) window on close, default is HIDE_ON_CLOSE
        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);

        // Catch escape key presses and have them close the dialog by mapping the escape keystroke to a custom dispose Action
        InputMap inputMap = contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
        ActionMap actionMap = contentPane.getActionMap();
        AbstractAction disposeAction = new AbstractAction() {
            public void actionPerformed(ActionEvent e){
                if(keyboardDisposalEnabled)
                    cancel();
            }
        };

        // Maps the dispose action to the 'Escape' keystroke
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), CUSTOM_DISPOSE_EVENT);
        actionMap.put(CUSTOM_DISPOSE_EVENT, disposeAction);

        // Maps the dispose action to the 'Apple+W' keystroke under Mac OS X
        if(OsFamily.MAC_OS.isCurrent())
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, ActionEvent.META_MASK), CUSTOM_DISPOSE_EVENT);

        // Under Windows, Alt+F4 automagically disposes the dialog, nothing to do
    }

    /**
     * Method called when the user has canceled through the escape key.
     * <p>
     * This method is equivalent to a call to {@link #dispose()}. It's meant to be
     * overriden by those implementations of <code>FocusDialog</code> that need to run
     * code before canceling the dialog.
     * </p>
     */
    public void cancel() {
        dispose();
    }


    /**
     * Sets the component that will receive focus once this dialog has been made visible.
     *
     * @param initialFocusComponent the component that will receive focus once this dialog has been made visible, if
     * null, the first component in the dialog will receive focus.
     */
    public void setInitialFocusComponent(JComponent initialFocusComponent) {
        this.initialFocusComponent = initialFocusComponent;

        if(initialFocusComponent==null)
            removeWindowListener(this);
        else
            addWindowListener(this);
    }


    /**
     * Sets a maximum width and height for this dialog.
     */
    @Override
    public void setMaximumSize(Dimension dimension) {
        this.maximumDimension = dimension;
    }

    /**
     * Sets a minium width and height for this dialog.
     */
    @Override
    public void setMinimumSize(Dimension dimension) {
        this.minimumDimension = dimension;
    }


    /**
     * Specifies whether this dialog can be automatically disposed using the 'Escape' key and 'Apple+W' under Mac OS X.
     * If enabled, {@link #dispose()} will be called when one of those keystrokes is pressed from any component
     * within this dialog.
     *
     * @param enabled true to enable automatic keyboard disposal, false to disable it
     */
    public void setKeyboardDisposalEnabled(boolean enabled) {
        this.keyboardDisposalEnabled = enabled;
    }


    /**
     * Overrides Window.pack() to take into account minimum and maximum dialog size (if specified).
     */
    @Override
    public void pack()  {
        super.pack();
        if(maximumDimension!=null)
            DialogToolkit.fitToMaxDimension(this, maximumDimension);
        else
            DialogToolkit.fitToScreen(this);

        if(minimumDimension!=null)
            DialogToolkit.fitToMinDimension(this, minimumDimension);
    }

    /**
     * Packs this dialog, makes it non-resizable and visible.
     */
    public void showDialog() {
        pack();

        if (locationRelativeComp == null) {
            DialogToolkit.centerOnScreen(this);
        } else {
            setLocation(
                    locationRelativeComp.getX() + (locationRelativeComp.getWidth() - getWidth()) / 2,
                    locationRelativeComp.getY() + (locationRelativeComp.getHeight() - getHeight()) / 2);
        }
        setVisible(true);
    }

    ////////////////////////////
    // WindowListener methods //
    ////////////////////////////

    public void windowOpened(WindowEvent e) {
    }

    public void windowActivated(WindowEvent e) {
        // (this method is called each time the dialog is activated)
        if (!firstTimeActivated && initialFocusComponent!=null) {
            LOGGER.trace("requesting focus on initial focus component");

            // First try using requestFocusInWindow() which is preferred over requestFocus(). If it fails
            // (returns false), call requestFocus:
            // "The focus behavior of this method can be implemented uniformly across platforms, and thus developers are
            // strongly encouraged to use this method over requestFocus when possible. Code which relies on requestFocus
            // may exhibit different focus behavior on different platforms."
            if (!initialFocusComponent.requestFocusInWindow()) {
                LOGGER.trace("requestFocusInWindow failed, calling requestFocus");
                FocusRequester.requestFocus(initialFocusComponent);
            }

            firstTimeActivated = true;
        }
    }

    public void windowClosing(WindowEvent e) {
    }

    public void windowClosed(WindowEvent e) {
    }

    public void windowDeactivated(WindowEvent e) {
    }

    public void windowIconified(WindowEvent e) {
    }

    public void windowDeiconified(WindowEvent e) {
    }
}
