ch.randelshofer.quaqua.QuaquaComboPopup Maven / Gradle / Ivy
Show all versions of Quaqua Show documentation
/*
* @(#)QuaquaComboPopup.java
*
* Copyright (c) 2004-2010 Werner Randelshofer, Immensee, Switzerland.
* All rights reserved.
*
* You may not use, copy or modify this file, except in compliance with the
* license agreement you entered into with Werner Randelshofer.
* For details see accompanying license terms.
*/
package ch.randelshofer.quaqua;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.plaf.basic.*;
import java.io.Serializable;
import java.beans.*;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ListDataListener;
import javax.swing.event.ListSelectionListener;
/**
* QuaquaComboPopup.
*
* @author Werner Randelshofer
* @version $Id: QuaquaComboPopup.java 361 2010-11-21 11:19:20Z wrandelshofer $
*/
public class QuaquaComboPopup extends BasicComboPopup {
private QuaquaComboBoxUI qqui;
private Handler handler;
private Component lastFocused;
private JRootPane invokerRootPane;
private boolean focusTraversalKeysEnabled;
/** comboCellBorder is used to accommodate the cell in the combo popup. */
private final static Border comboCellBorder = new EmptyBorder(0,7,0,7);
public QuaquaComboPopup(JComboBox cBox, QuaquaComboBoxUI qqui) {
super(cBox);
this.qqui = qqui;
updateCellRenderer(qqui.isTableCellEditor());
}
/**
* Implementation of ComboPopup.show().
*/
@Override
public void show() {
//System.out.println("QuaquaComboPopup@"+hashCode()+".show()");
setListSelection(comboBox.getSelectedIndex());
Point location = getPopupLocation();
show(comboBox, location.x, location.y);
// Must be done here after the call to show(..), because show does
// replace the UI.
setBorder(UIManager.getBorder("ComboBox.popupBorder"));
// This is required to properly render the selection, when the JComboBox
// is used as a table cell editor.
list.repaint();
}
/**
* Implementation of ComboPopup.hide().
*/
@Override
public void hide() {
//System.out.println("QuaquaComboPopup@"+hashCode()+".hide()");
super.hide();
//removeListenersAndResetFocus();
}
@Override
public void setVisible(boolean newValue) {
super.setVisible(newValue);
if (newValue) {
if (!comboBox.isEditable() && comboBox != KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner()) {
//
// remember current focus owner
lastFocused = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
// request focus on root pane and install keybindings
// used for menu navigation
invokerRootPane = SwingUtilities.getRootPane(comboBox);
if (invokerRootPane != null) {
//invokerRootPane.addFocusListener(rootPaneFocusListener);
invokerRootPane.requestFocus(true);
invokerRootPane.addKeyListener(getHandler());
focusTraversalKeysEnabled = invokerRootPane.getFocusTraversalKeysEnabled();
invokerRootPane.setFocusTraversalKeysEnabled(false);
/* InputMap menuInputMap = comboBox.getInputMap(popup, invokerRootPane);
addUIInputMap(invokerRootPane, menuInputMap);
addUIActionMap(invokerRootPane, menuActionMap);*/
}
}
} else {
if (lastFocused != null) {
if (!lastFocused.requestFocusInWindow()) {
// Workarounr for 4810575.
// If lastFocused is not in currently focused window
// requestFocusInWindow will fail. In this case we must
// request focus by requestFocus() if it was not
// transferred from our popup.
Window cfw = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow();
if (cfw != null &&
"###focusableSwingPopup###".equals(cfw.getName())) {
lastFocused.requestFocus();
}
}
lastFocused = null;
}
if (invokerRootPane != null) {
invokerRootPane.removeKeyListener(getHandler());
invokerRootPane.setFocusTraversalKeysEnabled(focusTraversalKeysEnabled);
//removeUIInputMap(invokerRootPane, menuInputMap);
//removeUIActionMap(invokerRootPane, menuActionMap);
invokerRootPane = null;
}
// receivedKeyPressed = false;
}
}
private void updateCellRenderer(boolean isTableCellEditor) {
list.setCellRenderer(
new QuaquaComboBoxCellRenderer(
comboBox.getRenderer(), isTableCellEditor, comboBox.isEditable()));
}
private int getMaximumRowCount() {
return (isEditable() || isTableCellEditor()) ? comboBox.getMaximumRowCount() : 100;
}
/**
* Calculates the upper left location of the Popup.
*/
private Point getPopupLocation() {
Dimension popupSize = comboBox.getSize();
Insets insets = getInsets();
// reduce the width of the scrollpane by the insets so that the popup
// is the same width as the combo box.
popupSize.setSize(popupSize.width - (insets.right + insets.left),
getPopupHeightForRowCount(getMaximumRowCount()));
Rectangle popupBounds = computePopupBounds(0, comboBox.getBounds().height,
popupSize.width, popupSize.height);
Dimension scrollSize = popupBounds.getSize();
Point popupLocation = popupBounds.getLocation();
scroller.setMaximumSize(scrollSize);
scroller.setPreferredSize(scrollSize);
scroller.setMinimumSize(scrollSize);
list.revalidate();
return popupLocation;
}
/**
* Sets the list selection index to the selectedIndex. This
* method is used to synchronize the list selection with the
* combo box selection.
*
* @param selectedIndex the index to set the list
*/
private void setListSelection(int selectedIndex) {
if (selectedIndex == -1) {
list.clearSelection();
} else {
list.setSelectedIndex(selectedIndex);
list.ensureIndexIsVisible(selectedIndex);
}
}
/**
* Calculate the placement and size of the popup portion of the combo box based
* on the combo box location and the enclosing screen bounds. If
* no transformations are required, then the returned rectangle will
* have the same values as the parameters.
*
* @param px starting x location
* @param py starting y location
* @param pw starting width
* @param ph starting height
* @return a rectangle which represents the placement and size of the popup
*/
@Override
protected Rectangle computePopupBounds(int px, int py, int pw, int ph) {
Toolkit toolkit = Toolkit.getDefaultToolkit();
Rectangle screenBounds;
int listWidth = getList().getPreferredSize().width;
Insets margin = qqui.getMargin();
boolean isTableCellEditor = isTableCellEditor();
boolean hasScrollBars = hasScrollBars();
boolean isEditable = isEditable();
if (isTableCellEditor) {
if (hasScrollBars) {
pw = Math.max(pw, listWidth + 16);
} else {
pw = Math.max(pw, listWidth);
}
} else {
if (hasScrollBars) {
px += margin.left;
pw = Math.max(pw - margin.left - margin.right, listWidth + 16);
} else {
if (isEditable) {
px += margin.left;
pw = Math.max(pw - qqui.getArrowWidth() - margin.left, listWidth);
} else {
px += margin.left;
pw = Math.max(pw - qqui.getArrowWidth() - margin.left, listWidth);
}
}
}
// Take extended cell border into account which is used in
// QuaquaListUI.
Insets cellBorderInsets = comboCellBorder.getBorderInsets(this);
pw+=cellBorderInsets.left+cellBorderInsets.right;
// Calculate the desktop dimensions relative to the combo box.
GraphicsConfiguration gc = comboBox.getGraphicsConfiguration();
Point p = new Point();
SwingUtilities.convertPointFromScreen(p, comboBox);
if (gc != null) {
// Get the screen insets.
Insets screenInsets=toolkit.getScreenInsets(gc);
// Note: We must create a new rectangle here, because method
// getBounds does not return a copy of a rectangle on J2SE 1.3.
screenBounds = new Rectangle(gc.getBounds());
screenBounds.width -= (screenInsets.left + screenInsets.right);
screenBounds.height -= (screenInsets.top + screenInsets.bottom);
screenBounds.x += screenInsets.left;
screenBounds.y += screenInsets.top;
} else {
screenBounds = new Rectangle(p, toolkit.getScreenSize());
}
if (isDropDown()) {
if (!isTableCellEditor) {
if (isEditable) {
py -= margin.bottom + 2;
} else {
py -= margin.bottom;
}
}
} else {
int yOffset;
if (isTableCellEditor) {
yOffset = 7;
} else {
yOffset = 3 - margin.top;
}
int selectedIndex = comboBox.getSelectedIndex();
if (selectedIndex <= 0) {
py = -yOffset;
} else {
py = -yOffset - list.getCellBounds(0, selectedIndex - 1).height;
}
}
// Add the preferred scroll bar width, if the popup does not fit
// on the available rectangle.
Rectangle rect = new Rectangle(px-p.x,py-p.y,pw,ph);
if (screenBounds.height < ph) {
rect.width += 16;
}
// Compute the rectangle for the popup menu
rect = screenBounds.intersection(rect);
rect.x+=p.x;
rect.y+=p.y;
return rect;
}
private boolean isDropDown() {
return comboBox.isEditable() || hasScrollBars();
}
private boolean hasScrollBars() {
return comboBox.getModel().getSize() > getMaximumRowCount();
}
private boolean isEditable() {
return comboBox.isEditable();
}
private boolean isTableCellEditor() {
return qqui.isTableCellEditor();
}
/**
* Configures the popup portion of the combo box. This method is called
* when the UI class is created.
*/
@Override
protected void configurePopup() {
super.configurePopup();
// FIXME - We need to convert the border into a non-UIResource object.
// An UIResourceObject will be removed from the popup.
//setBorder( new CompoundBorder(UIManager.getBorder("PopupMenu.border"), new EmptyBorder(0,0,0,0)));
setBorder(UIManager.getBorder("ComboBox.popupBorder"));
setFocusable(true);
}
@Override
protected void configureList() {
super.configureList();
list.setBackground(UIManager.getColor("PopupMenu.background"));
list.setFocusable(true);
list.setRequestFocusEnabled(true);
list.putClientProperty("Quaqua.List.style", "comboPopup");
}
/**
* Creates a listener
* that will watch for mouse-press and release events on the combo box.
*
* Warning:
* When overriding this method, make sure to maintain the existing
* behavior.
*
* @return a MouseListener
which will be added to
* the combo box or null
*/
@Override
protected MouseListener createMouseListener() {
return getHandler();
}
/**
* Creates the mouse motion listener which will be added to the combo
* box.
*
* Warning:
* When overriding this method, make sure to maintain the existing
* behavior.
*
* @return a MouseMotionListener
which will be added to
* the combo box or null
*/
@Override
protected MouseMotionListener createMouseMotionListener() {
return getHandler();
}
/**
* Creates the key listener that will be added to the combo box. If
* this method returns null then it will not be added to the combo box.
*
* @return a KeyListener
or null
*/
@Override
protected KeyListener createKeyListener() {
return getHandler();
}
/**
* Creates a list selection listener that watches for selection changes in
* the popup's list. If this method returns null then it will not
* be added to the popup list.
*
* @return an instance of a ListSelectionListener
or null
*/
@Override
protected ListSelectionListener createListSelectionListener() {
return null;
}
/**
* Creates a list data listener which will be added to the
* ComboBoxModel
. If this method returns null then
* it will not be added to the combo box model.
*
* @return an instance of a ListDataListener
or null
*/
@Override
protected ListDataListener createListDataListener() {
return null;
}
/**
* Creates a mouse listener that watches for mouse events in
* the popup's list. If this method returns null then it will
* not be added to the combo box.
*
* @return an instance of a MouseListener
or null
*/
@Override
protected MouseListener createListMouseListener() {
return getHandler();
}
/**
* Creates a mouse motion listener that watches for mouse motion
* events in the popup's list. If this method returns null then it will
* not be added to the combo box.
*
* @return an instance of a MouseMotionListener
or null
*/
@Override
protected MouseMotionListener createListMouseMotionListener() {
return getHandler();
}
/**
* Creates a PropertyChangeListener
which will be added to
* the combo box. If this method returns null then it will not
* be added to the combo box.
*
* @return an instance of a PropertyChangeListener
or null
*/
@Override
protected PropertyChangeListener createPropertyChangeListener() {
return getHandler();
}
/**
* Creates an ItemListener
which will be added to the
* combo box. If this method returns null then it will not
* be added to the combo box.
*
* Subclasses may override this method to return instances of their own
* ItemEvent handlers.
*
* @return an instance of an ItemListener
or null
*/
@Override
protected ItemListener createItemListener() {
return getHandler();
}
private Handler getHandler() {
if (handler == null) {
handler = new Handler();
}
return handler;
}
private class Handler implements ItemListener, MouseListener,
MouseMotionListener, PropertyChangeListener,
Serializable, KeyListener {
//
// MouseListener
// NOTE: this is added to both the JList and JComboBox
//
public void mouseClicked(MouseEvent e) {
}
public void mousePressed(MouseEvent e) {
if (e.getSource() == list) {
return;
}
if (!SwingUtilities.isLeftMouseButton(e) || !comboBox.isEnabled()) {
return;
}
if (comboBox.isEditable()) {
Component comp = comboBox.getEditor().getEditorComponent();
if ((!(comp instanceof JComponent)) || ((JComponent) comp).isRequestFocusEnabled()) {
comp.requestFocus();
}
} else if (comboBox.isRequestFocusEnabled()) {
comboBox.requestFocus();
}
togglePopup();
}
public void mouseReleased(MouseEvent e) {
if (e.getSource() == list) {
if (list.getModel().getSize() > 0) {
// JList mouse listener
if (comboBox.getSelectedIndex() != list.getSelectedIndex()) {
comboBox.setSelectedIndex(list.getSelectedIndex());
} else {
comboBox.getEditor().setItem(list.getSelectedValue());
}
}
comboBox.setPopupVisible(false);
// workaround for cancelling an edited item (bug 4530953)
if (comboBox.isEditable() && comboBox.getEditor() != null) {
comboBox.configureEditor(comboBox.getEditor(),
comboBox.getSelectedItem());
}
return;
}
// JComboBox mouse listener
Component source = (Component) e.getSource();
Dimension size = source.getSize();
Rectangle bounds = new Rectangle(0, 0, size.width - 1, size.height - 1);
if (!bounds.contains(e.getPoint())) {
MouseEvent newEvent = convertMouseEvent(e);
Point location = newEvent.getPoint();
Rectangle r = new Rectangle();
list.computeVisibleRect(r);
if (r.contains(location)) {
if (comboBox.getSelectedIndex() != list.getSelectedIndex()) {
comboBox.setSelectedIndex(list.getSelectedIndex());
} else {
comboBox.getEditor().setItem(list.getSelectedValue());
}
}
comboBox.setPopupVisible(false);
}
hasEntered = false;
stopAutoScrolling();
}
public void mouseEntered(MouseEvent e) {
}
public void mouseExited(MouseEvent e) {
}
//
// MouseMotionListener:
// NOTE: this is added to both the List and ComboBox
//
public void mouseMoved(MouseEvent anEvent) {
if (anEvent.getSource() == list) {
Point location = anEvent.getPoint();
Rectangle r = new Rectangle();
list.computeVisibleRect(r);
if (r.contains(location)) {
updateListBoxSelectionForEvent(anEvent, false);
}
}
}
public void mouseDragged(MouseEvent e) {
if (e.getSource() == list) {
return;
}
if (isVisible()) {
MouseEvent newEvent = convertMouseEvent(e);
Rectangle r = new Rectangle();
list.computeVisibleRect(r);
if (newEvent.getPoint().y >= r.y && newEvent.getPoint().y <= r.y + r.height - 1) {
hasEntered = true;
if (isAutoScrolling) {
stopAutoScrolling();
}
Point location = newEvent.getPoint();
if (r.contains(location)) {
updateListBoxSelectionForEvent(newEvent, false);
}
} else {
if (hasEntered) {
int directionToScroll = newEvent.getPoint().y < r.y ? SCROLL_UP : SCROLL_DOWN;
if (isAutoScrolling && scrollDirection != directionToScroll) {
stopAutoScrolling();
startAutoScrolling(directionToScroll);
} else if (!isAutoScrolling) {
startAutoScrolling(directionToScroll);
}
} else {
if (e.getPoint().y < 0) {
hasEntered = true;
startAutoScrolling(SCROLL_UP);
}
}
}
}
}
//
// PropertyChangeListener
//
public void propertyChange(PropertyChangeEvent e) {
JComboBox comboBox = (JComboBox) e.getSource();
String propertyName = e.getPropertyName();
if (propertyName == "model") {
ComboBoxModel oldModel = (ComboBoxModel) e.getOldValue();
ComboBoxModel newModel = (ComboBoxModel) e.getNewValue();
uninstallComboBoxModelListeners(oldModel);
installComboBoxModelListeners(newModel);
list.setModel(newModel);
if (isVisible()) {
hide();
}
} else if (propertyName == "renderer") {
list.setCellRenderer(comboBox.getRenderer());
if (isVisible()) {
hide();
}
} else if (propertyName == "componentOrientation") {
// Pass along the new component orientation
// to the list and the scroller
ComponentOrientation o = (ComponentOrientation) e.getNewValue();
JList list = getList();
if (list != null && list.getComponentOrientation() != o) {
list.setComponentOrientation(o);
}
if (scroller != null && scroller.getComponentOrientation() != o) {
scroller.setComponentOrientation(o);
}
if (o != getComponentOrientation()) {
setComponentOrientation(o);
}
} else if (propertyName == "lightWeightPopupEnabled") {
setLightWeightPopupEnabled(comboBox.isLightWeightPopupEnabled());
} else if (propertyName.equals("renderer") ||
propertyName.equals(QuaquaComboBoxUI.IS_TABLE_CELL_EDITOR)) {
updateCellRenderer(e.getNewValue().equals(Boolean.TRUE));
} else if (propertyName.equals("JComboBox.lightweightKeyboardNavigation")) {
// In Java 1.3 we have to use this property to guess whether we
// are a table cell editor or not.
updateCellRenderer(e.getNewValue() != null && e.getNewValue().equals("Lightweight"));
} else if (propertyName.equals("editable")) {
updateCellRenderer(isTableCellEditor());
}
}
//
// ItemListener
//
public void itemStateChanged(ItemEvent e) {
if (e.getStateChange() == ItemEvent.SELECTED) {
JComboBox comboBox = (JComboBox) e.getSource();
setListSelection(comboBox.getSelectedIndex());
}
}
public void keyTyped(KeyEvent e) {
}
public void keyPressed(KeyEvent e) {
processKeyEvent(e, true);
// Forward key pressed events to QuaquaComboBoxUI when the
// popup us showing, but the combo box is not focused.
if (!e.isConsumed() && !comboBox.isEditable() && !comboBox.isFocusOwner()) {
QuaquaComboBoxUI ui = (QuaquaComboBoxUI) comboBox.getUI();
ui.getKeyListener().keyPressed(e);
}
}
public void keyReleased(KeyEvent e) {
processKeyEvent(e, false);
}
private boolean processKeyEvent(KeyEvent e, boolean pressed) {
// Forward key pressed events to QuaquaComboBoxUI when the
// popup us showing, but the combo box is not focused.
if (!e.isConsumed() && !comboBox.isEditable() && !comboBox.isFocusOwner()) {
int condition = WHEN_ANCESTOR_OF_FOCUSED_COMPONENT;
// Get the KeyStroke
KeyStroke ks;
if (e.getID() == KeyEvent.KEY_TYPED) {
ks = KeyStroke.getKeyStroke(e.getKeyChar());
} else {
ks = KeyStroke.getKeyStroke(e.getKeyCode(), e.getModifiers(),
(pressed ? false : true));
}
InputMap map = comboBox.getInputMap(condition/*, false*/);
ActionMap am = comboBox.getActionMap(/*false*/);
// System.out.println("QuaquaComboPopup@"+QuaquaComboPopup.this.hashCode()+".processKeyEvent " + ks);
if (map != null && am != null && isEnabled()) {
Object binding = map.get(ks);
// System.out.println(" binding: " + binding);
Action action = (binding == null) ? null : am.get(binding);
if (action != null) {
e.consume();
return SwingUtilities.notifyAction(action, ks, e, comboBox,
e.getModifiers());
}
}
}
return false;
}
}
//
// end Event Listeners
//=================================================================
}