blob: f851b0060dc48d7792a5cf8ee838040dbf3ed83a [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2008 Oracle. All rights reserved.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0, which accompanies this distribution
* and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Contributors:
* Oracle - initial API and implementation
******************************************************************************/
package org.eclipse.jpt.utility.internal.swing;
import java.awt.AWTEvent;
import java.awt.AWTException;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.Point;
import java.awt.Robot;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.ListCellRenderer;
import javax.swing.SwingConstants;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.plaf.basic.BasicComboBoxUI;
import org.eclipse.jpt.utility.internal.ClassTools;
/**
* This component provides a way to handle selecting an item from a
* list that may grow too large to be handled conveniently by a combo-box.
* If the list's size is less than the designated "long" list size,
* the choice list will be displayed in a normal combo-box popup;
* otherwise, a dialog will be used to prompt the user to choose a selection.
*
* To change the browse mechanism, subclasses may
* - override the method #buildBrowser()
* - override the method #browse(), in which case the method
* #buildBrowser() may be ignored.
*/
public class ListChooser
extends JComboBox
{
/** the size of a "long" list - anything smaller is a "short" list */
int longListSize = DEFAULT_LONG_LIST_SIZE;
/** the default size of a "long" list, which is 20 (to match JOptionPane's behavior) */
public static final int DEFAULT_LONG_LIST_SIZE = 20;
/** property change associated with long list size */
public static final String LONG_LIST_SIZE_PROPERTY = "longListSize"; //$NON-NLS-1$
static JLabel prototypeLabel = new JLabel("Prototype", new EmptyIcon(17), SwingConstants.LEADING); //$NON-NLS-1$
/**
* whether the chooser is choosable. if a chooser is not choosable,
* it only serves as a display widget. a user may not change its
* selected value.
*/
boolean choosable = true;
/** property change associated with choosable */
public static final String CHOOSABLE_PROPERTY = "choosable"; //$NON-NLS-1$
/** the browser used to make a selection from the long list - typically via a dialog */
private ListBrowser browser;
private NodeSelector nodeSelector;
/** INTERNAL - The popup is being shown. Used to prevent infinite loop. */
boolean popupAlreadyInProgress;
// **************** Constructors ******************************************
/**
* Construct a list chooser for the specified model.
*/
public ListChooser(ComboBoxModel model) {
this(model, new NodeSelector.DefaultNodeSelector());
}
public ListChooser(CachingComboBoxModel model) {
this(model, new NodeSelector.DefaultNodeSelector());
}
public ListChooser(ComboBoxModel model, NodeSelector nodeSelector) {
this(new NonCachingComboBoxModel(model), nodeSelector);
}
public ListChooser(CachingComboBoxModel model, NodeSelector nodeSelector) {
super(model);
this.initialize();
this.nodeSelector = nodeSelector;
}
// **************** Initialization ****************************************
protected void initialize() {
this.addPopupMenuListener(this.buildPopupMenuListener());
this.setRenderer(new DefaultListCellRenderer());
this.addKeyListener(buildF3KeyListener());
//These are used to workaround problems with Swing trying to
//determine the size of a comboBox with a large model
setPrototypeDisplayValue(prototypeLabel);
listBox().setPrototypeCellValue(prototypeLabel);
}
private JList listBox() {
return (JList) ClassTools.fieldValue(this.ui, "listBox"); //$NON-NLS-1$
}
/**
* When the popup is about to be shown, the event is consumed, and
* PopupHandler determines whether to reshow the popup or to show
* the long list browser.
*/
private PopupMenuListener buildPopupMenuListener() {
return new PopupMenuListener() {
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
ListChooser.this.aboutToShowPopup();
}
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
// do nothing
}
public void popupMenuCanceled(PopupMenuEvent e) {
// do nothing
}
@Override
public String toString() {
return "pop-up menu listener"; //$NON-NLS-1$
}
};
}
/**
* If this code is being reached due to the PopupHandler already being in progress,
* then do nothing. Otherwise, set the flag to true and launch the PopupHandler.
*/
void aboutToShowPopup() {
if (this.popupAlreadyInProgress) {
return;
}
this.popupAlreadyInProgress = true;
EventQueue.invokeLater(new PopupHandler());
}
private KeyListener buildF3KeyListener() {
return new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_F3) {
goToSelectedItem();
}
}
@Override
public String toString() {
return "F3 key listener"; //$NON-NLS-1$
}
};
}
public void goToSelectedItem() {
if (getSelectedItem() != null) {
ListChooser.this.nodeSelector.selectNodeFor(getSelectedItem());
}
}
// **************** Browsing **********************************************
/**
* Lazily initialize because subclasses may have further initialization to do
* before browser can be built.
*/
protected void browse() {
if (this.browser == null) {
this.browser = this.buildBrowser();
}
this.browser.browse(this);
}
/**
* Return the "browser" used to make a selection from the long list,
* typically via a dialog.
*/
protected ListChooser.ListBrowser buildBrowser() {
return new SimpleListBrowser();
}
// **************** Choosable functionality *******************************
/** override behavior - consume selection if chooser is not choosable */
@Override
public void setSelectedIndex(int anIndex) {
if (this.choosable) {
super.setSelectedIndex(anIndex);
}
}
private void updateArrowButton() {
try {
BasicComboBoxUI comboBoxUi = (BasicComboBoxUI) ListChooser.this.getUI();
JButton arrowButton = (JButton) ClassTools.fieldValue(comboBoxUi, "arrowButton"); //$NON-NLS-1$
arrowButton.setEnabled(this.isEnabled() && this.choosable);
}
catch (Exception e) {
// this is a huge hack to try and make the combo box look right,
// so if it doesn't work, just swallow the exception
}
}
// **************** List Caching *******************************
void cacheList() {
((CachingComboBoxModel) getModel()).cacheList();
}
void uncacheList() {
((CachingComboBoxModel) getModel()).uncacheList();
}
boolean listIsCached() {
return ((CachingComboBoxModel) getModel()).isCached();
}
// **************** Public ************************************************
public int longListSize() {
return this.longListSize;
}
public void setLongListSize(int newLongListSize) {
int oldLongListSize = this.longListSize;
this.longListSize = newLongListSize;
this.firePropertyChange(LONG_LIST_SIZE_PROPERTY, oldLongListSize, newLongListSize);
}
public boolean isChoosable() {
return this.choosable;
}
public void setChoosable(boolean newValue) {
boolean oldValue = this.choosable;
this.choosable = newValue;
this.firePropertyChange(CHOOSABLE_PROPERTY, oldValue, newValue);
this.updateArrowButton();
}
// **************** Handle selecting null as a value **********************
private boolean selectedIndexIsNoneSelectedItem(int index) {
return index == -1 &&
getModel().getSize() > 0 &&
getModel().getElementAt(0) == null;
}
@Override
public int getSelectedIndex() {
boolean listNotCached = !listIsCached();
if (listNotCached) {
cacheList();
}
int index = super.getSelectedIndex();
// Use index 0 to show the <none selected> item since the actual value is
// null and JComboBox does not handle null values
if (selectedIndexIsNoneSelectedItem(index)) {
index = 0;
}
if (listNotCached) {
uncacheList();
}
return index;
}
//wrap the renderer to deal with the prototypeDisplayValue
@Override
public void setRenderer(final ListCellRenderer aRenderer) {
super.setRenderer(new ListCellRenderer(){
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
if (value == prototypeLabel) {
return prototypeLabel;
}
return aRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
}
});
}
// **************** Member classes ****************************************
/**
* Define the API required by this ListChooser when it must
* prompt the user to select an item from the "long" list.
*/
public interface ListBrowser
{
/**
* Prompt the user to make a selection from the specified
* combo-box's model.
*/
void browse(ListChooser parentChooser);
}
/**
* Runnable class that consumes popup window and determines whether
* to reshow popup or to launch browser, based on the size of the list.
*/
private class PopupHandler
implements Runnable
{
/** The mouse event */
private MouseEvent lastMouseEvent;
/** The component from which the last mouse event was thrown */
private JComponent eventComponent;
/** The location of the component at the time the last mouse event was thrown */
private Point componentLocation;
/** The location of the mouse at the time the last mouse event was thrown */
private Point mouseLocation;
PopupHandler() {
this.initialize();
}
private void initialize() {
AWTEvent event = EventQueue.getCurrentEvent();
if (event instanceof MouseEvent) {
this.lastMouseEvent = (MouseEvent) event;
this.eventComponent = (JComponent) this.lastMouseEvent.getSource();
this.componentLocation = this.eventComponent.getLocationOnScreen();
this.mouseLocation = this.lastMouseEvent.getPoint();
}
else {
this.eventComponent = null;
this.componentLocation = null;
this.mouseLocation = null;
}
}
public void run() {
ListChooser.this.hidePopup();
cacheList();
if (ListChooser.this.choosable == true) {
// If the combo box model is of sufficient length, the browser will be shown.
// Asking the combo box model for its size should be enough to ensure that
// its size is recalculated.
if (ListChooser.this.getModel().getSize() > ListChooser.this.longListSize) {
this.checkComboBoxButton();
ListChooser.this.browse();
}
else {
ListChooser.this.showPopup();
this.checkMousePosition();
}
}
if (listIsCached()) {
uncacheList();
}
ListChooser.this.popupAlreadyInProgress = false;
}
/** If this is not done, the button never becomes un-pressed */
private void checkComboBoxButton() {
try {
BasicComboBoxUI comboBoxUi = (BasicComboBoxUI) ListChooser.this.getUI();
JButton arrowButton = (JButton) ClassTools.fieldValue(comboBoxUi, "arrowButton"); //$NON-NLS-1$
arrowButton.getModel().setPressed(false);
}
catch (Exception ex) {
// this is a huge hack to try and make the combo box look right,
// so if it doesn't work, just swallow the exception
this.handleException(ex);
}
}
private void handleException(@SuppressWarnings("unused") Exception ex) {
// do nothing for now
}
/**
* Moves the mouse back to its original position before any jiggery pokery that we've done.
*/
private void checkMousePosition() {
if (this.eventComponent == null) {
return;
}
final Point newComponentLocation = this.eventComponent.getLocationOnScreen();
boolean componentMoved =
newComponentLocation.x - this.componentLocation.x != 0
|| newComponentLocation.y - this.componentLocation.y != 0;
if (componentMoved) {
try {
new Robot().mouseMove(
newComponentLocation.x + this.mouseLocation.x,
newComponentLocation.y + this.mouseLocation.y
);
}
catch (AWTException ex) {
// move failed - do nothing
}
}
}
}
}