/*
 * ECatalog is a database front-end, with two main features:
 * 1. Use of preferences
 *  A preference-based approach, where the user is allowed to define the importance of each criterion.
 *  Then the items are ranked accordingly to his criteria.
 * 2. Trade-off analysis
 *  A cooperative database approach, where the system "argues" with the user about his criteria.
 *  When there are no matching items, the system explains the minimal conflicting set and
 *  give some possible strong and weak relaxations about his criteria.
 * This package also containts the software and the set-up details used for our User Study,
 * comparing the use or not of the two previous features mentioned above.
 *
 * Copyright (C) 2006 David Portabella Clotet, Artificial Intelligence Laboratory, EPFL
 * 
 * This file is part of ecatalog-1.0.zip
 * 
 * ECatalog is free software and a free user study set-up;
 * 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 2 of the License, or
 * (at your option) any later version.
 * 
 * ECatalog 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 ECatalog; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 * @version 1.0
 * @author David Portabella
 * To contact the author:
 * email: david@portabella.name and david.portabella@epfl.ch
 * 
 * More information about ECatalog:
 *  http://sourceforge.net/projects/ecatalog/
 *  http://icwww.epfl.ch/~portabel/ecatalogs/
 */

/*
 *	Modulname:	HeaderedComboBox
 *	Autor:		Eyer Leander
 *	Datum:		27.01.2006
 *
 *	(c) Copyright 2005 by
 *		Eyer IT Services, Naters
 *	All rights reserved
 */
package utils;

import javax.swing.*;
import javax.swing.plaf.metal.MetalComboBoxUI;
import javax.swing.plaf.basic.ComboPopup;
import javax.swing.plaf.basic.BasicComboPopup;
import javax.swing.plaf.basic.BasicComboBoxUI;
import javax.swing.plaf.ComboBoxUI;
import java.util.Vector;
import java.awt.*;
import java.awt.event.*;


import ecatalog.gui.MainGui;  //TODO minor: hack for createToolTip

/**
 * A Combobox with a header component. Is set by the "setHeaderComponent()" method and
 * can be removed again by calling it with parameter null.
 */
public class HeaderedComboBox extends JComboBox {
    Dimension popUpPreferredSize = null;

    /**
     * The Component painted at the top of the combo box list
     */
    private JComponent headerComponent = null;

    /**
     * Indicates if the popup might be wider than the combobox
     */
    private boolean isStepped = false;

    /**
     * Overloaded Constructors: We keep the same interface
     */
    public HeaderedComboBox() {
	super();
	setUI(new HeaderedComboBoxUI());
    }

    /**
     * Overloaded Constructors: We keep the same interface
     */
    public HeaderedComboBox(ComboBoxModel m) {
	super(m);
	setUI(new HeaderedComboBoxUI());
    }

    /**
     * Overloaded Constructors: We keep the same interface
     */
    public HeaderedComboBox(Object[] items) {
	super(items);
	setUI(new HeaderedComboBoxUI());
    }

    /**
     * Overloaded Constructors: We keep the same interface
     */
    public HeaderedComboBox(Vector v) {
	super(v);
	setLightWeightPopupEnabled(false);
    }

    /**
     * Return the currently installed Header Component
     */
    public JComponent getHeaderComponent() {
	return headerComponent;
    }

    /**
     * Set the header component for this combo box. The header is deactivated
     * if the parameter is null.
     *
     * @param headerComponent
     */
    public void setHeaderComponent(JComponent headerComponent) {
	this.headerComponent = headerComponent;
	setUI(new HeaderedComboBoxUI());  //needs to be recreated, so that HeaderedComboBoxUI use the new headerComponent.
    }

    /**
     * Returns true if the popup may be wider than the combobox
     *
     * @return
     */
    public boolean isStepped() {
	return isStepped;
    }

    /**
     * Set if the popup may be wider than the combobox
     *
     * @param stepped
     */
    public void setStepped(boolean stepped) {
	isStepped = stepped;
    }



    public Dimension getPopUpPreferredSize() {
	ComboBoxUI ui = getUI();
	if (!(ui instanceof HeaderedComboBoxUI))
	    return null;
	HeaderedComboBoxUI hui = (HeaderedComboBoxUI) ui;
	if (hui.getPopUp() == null || !(hui.getPopUp() instanceof HeaderedComboPopup))
	    return null;
	HeaderedComboPopup hcp = (HeaderedComboPopup) hui.getPopUp();
	return hcp.getPopUpPreferredSize();
    }

    /* From now on, the size of the popup will be always the same, independently of whether the number of items have changed */
    public void setPopUpPreferredSize(Dimension size) {
	//System.out.println("setPopUpPreferredSize:" + size);
	this.popUpPreferredSize = size;
    }

    public void scrollPopUpToBeginning() {
	ComboBoxUI ui = getUI();
	if (!(ui instanceof HeaderedComboBoxUI))
	    return;
	((HeaderedComboBoxUI)ui).scrollPopUpToBeginning();
	return;
    }

}

/**
 * The UI that installs the Headered Combo Popup
 */
class HeaderedComboBoxUI extends MetalComboBoxUI {

    /**
     * This method is overloaded to install our own Popup class
     */
    protected ComboPopup createPopup() {
	return new HeaderedComboPopup(comboBox);
    }

    /**
     * This method is overloaded to install our own focus handler
     * It will allow the popup to remain open while components inside the popup gain the focus.
     */
    protected FocusListener createFocusListener() {
	// take the existing focus handler as starting point
	return new BasicComboBoxUI.FocusHandler() {
		/**
		 * This method is called when the popup loses the input focus. Now we have to find
		 * out if the new owner is still somewhere inside the popup. If this is the case,
		 * keep the window open.
		 *
		 * @param evt
		 */
		public void focusLost(FocusEvent evt) {
		    Component newOwner = evt.getOppositeComponent();
		    Component runner = newOwner;
		    boolean isInPopup = false;
		    while (runner != null) {
			if (runner == popup) {
			    isInPopup = true;
			}
			runner = runner.getParent();
		    }

		    if (isInPopup == false) {
			super.focusLost(evt);
		    }
		}
	    };
    }

    void scrollPopUpToBeginning() {
	if (!(popup instanceof HeaderedComboPopup))
	    return;
	((HeaderedComboPopup)popup).scrollPopUpToBeginning();
    }

    HeaderedComboPopup getPopUp() { return (HeaderedComboPopup)popup; }
}

/**
 * The advanced ComboBox Popup with the additional Panel
 */
class HeaderedComboPopup extends BasicComboPopup {
    int maxWidth;

    /**
     * Component that's displayed over the list
     */
    private JComponent header = null;


    /**
     * The header is installed in the constructor
     */
    protected HeaderedComboPopup(JComboBox combo) {
	super(combo);

	if (combo instanceof HeaderedComboBox) {
	    header = ((HeaderedComboBox) combo).getHeaderComponent();
	    if (header != null) {
		JViewport viewPort = new JViewport();
		viewPort.setView(header);
		scroller.setColumnHeader(viewPort);
	    }
	}
	maxWidth = Toolkit.getDefaultToolkit().getScreenSize().width*3/4;
    }


    /* TODO minor: David, this is not the right place. It does not work anyway :-? */
    public JToolTip createToolTip() {
	return MainGui.createToolTip(); 
    }  


    /**
     * Solution for the text focus problem in stepped popups outside of the original frame.
     *
     * When the popup doesn't fit anymore in the drawing space of the window of it's combo box,
     * it will use a "Heavyweight" window (which boils down to it's own JWindow instance). That
     * heavyweight window is not focusable by default.
     *
     * The header of our combo box can contain textfields, which don't work if they are
     * placed in a window which is not focusable. Therefore we adjust the focusable property
     * of the top level window this popup is hosted in.
     */
    public void show() {
	super.show();
	Component runner = ((HeaderedComboBox) comboBox).getHeaderComponent();
	while (runner != null) {
	    runner = runner.getParent();
	    if (runner instanceof JWindow) {
		((JWindow) runner).setFocusableWindowState(true);
	    }
	}
    }

    /**
     * This metohd is overloaded to implement the stepping (larger popup than combobox).
     */
    protected Rectangle computePopupBounds(int px, int py, int pw, int ph) {
	Dimension popUpPreferredSize = ((HeaderedComboBox)comboBox).popUpPreferredSize;

	if (popUpPreferredSize != null) {
	    ph = popUpPreferredSize.height;
	    if (popUpPreferredSize.width != -1)
		pw = Math.max(pw, popUpPreferredSize.width);
	}
	//System.out.println("computePopupBounds, popUpPreferredSize = " + popUpPreferredSize + ", px=" +px + ", py="+py+",pw="+pw+",ph="+ph);
	Rectangle rect = super.computePopupBounds(px, py, pw, ph);
	return rect;
    }

    void scrollPopUpToBeginning() {
	JViewport viewport = (JViewport)list.getParent();
	viewport.setViewPosition(new Point(0, 0));
    }

    Dimension getPopUpPreferredSize() {
	Dimension size = new Dimension(list.getPreferredSize().width, getPopupHeightForRowCount(comboBox.getMaximumRowCount()));
	if (header != null)
	    size.height += header.getPreferredSize().height;

	if ((comboBox instanceof HeaderedComboBox) && ((HeaderedComboBox) comboBox).isStepped()) {
	    size.width += 20;  //DAVID - TODO: this list.fetPreferredSize().width, takes into account the values 
	    size.width = Math.min(size.width, maxWidth);
	} else {
	    size.width = -1; // no preference
	}
	return size;
    }
}

