com.jgoodies.looks.plastic.PlasticComboBoxUI Maven / Gradle / Ivy
Show all versions of jgoodies-looks Show documentation
/*
* Copyright (c) 2001-2012 JGoodies Karsten Lentzsch. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* o Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* o Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* o Neither the name of JGoodies Karsten Lentzsch nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jgoodies.looks.plastic;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.Rectangle;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.ComboBoxEditor;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JScrollBar;
import javax.swing.JTextField;
import javax.swing.JToolBar;
import javax.swing.ListCellRenderer;
import javax.swing.UIManager;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.TextUI;
import javax.swing.plaf.UIResource;
import javax.swing.plaf.basic.BasicComboBoxRenderer;
import javax.swing.plaf.basic.BasicComboBoxUI;
import javax.swing.plaf.basic.BasicComboPopup;
import javax.swing.plaf.basic.ComboPopup;
import javax.swing.plaf.metal.MetalComboBoxUI;
import javax.swing.plaf.metal.MetalScrollBarUI;
import javax.swing.plaf.metal.MetalTextFieldUI;
import com.jgoodies.looks.Options;
/**
* The JGoodies Plastic Look and Feel implementation of {@code ComboBoxUI}.
* Has the same height as text fields - unless you change the renderer.
*
* Also, this class offers to use the combo's popup prototype display value
* to compute the popup menu width. This is an optional feature of
* the JGoodies Plastic L&fs implemented via a client property key.
*
* @author Karsten Lentzsch
* @version $Revision: 1.19 $
*
* @see Options#COMBO_POPUP_PROTOTYPE_DISPLAY_VALUE_KEY
*/
public class PlasticComboBoxUI extends MetalComboBoxUI {
static final String CELL_EDITOR_KEY = "JComboBox.isTableCellEditor";
/**
* Used to determine the minimum height of a text field,
* which in turn is used to answer the combobox's minimum height.
*/
private static final JTextField PHANTOM = new JTextField("Phantom");
/**
* Different Plastic L&fs may need different phantom UIs.
* Therefore we store the LookAndFeel class and update the
* phantom UI whenever the Look&Feel changes.
*/
private static Class phantomLafClass;
private boolean tableCellEditor;
private PropertyChangeListener propertyChangeListener;
// ************************************************************************
public static ComponentUI createUI(JComponent b) {
ensurePhantomHasPlasticUI();
return new PlasticComboBoxUI();
}
/**
* Ensures that the phantom text field has a Plastic text field UI.
*/
private static void ensurePhantomHasPlasticUI() {
TextUI ui = PHANTOM.getUI();
Class lafClass = UIManager.getLookAndFeel().getClass();
if ( (phantomLafClass != lafClass)
|| !(ui instanceof MetalTextFieldUI)) {
phantomLafClass = lafClass;
PHANTOM.updateUI();
}
}
// ************************************************************************
@Override
public void installUI( JComponent c ) {
super.installUI(c);
tableCellEditor = isTableCellEditor();
}
@Override
protected void installListeners() {
super.installListeners();
propertyChangeListener = new TableCellEditorPropertyChangeHandler();
comboBox.addPropertyChangeListener(CELL_EDITOR_KEY, propertyChangeListener);
}
@Override
protected void uninstallListeners() {
super.uninstallListeners();
comboBox.removePropertyChangeListener(CELL_EDITOR_KEY, propertyChangeListener);
propertyChangeListener = null;
}
// Overridden Superclass Configuration ************************************
/**
* Creates and answers the arrow button that is to be used in the combo box.
*
* Overridden to use a button that can have a pseudo 3D effect.
*/
@Override
protected JButton createArrowButton() {
return new PlasticComboBoxButton(
comboBox,
PlasticIconFactory.getComboBoxButtonIcon(),
comboBox.isEditable(),
currentValuePane,
listBox);
}
/**
* Creates the editor that is to be used in editable combo boxes.
* This method only gets called if a custom editor has not already
* been installed in the JComboBox.
*/
@Override
protected ComboBoxEditor createEditor() {
return new PlasticComboBoxEditor.UIResource(tableCellEditor);
}
/**
* Creates a layout manager for managing the components which
* make up the combo box.
*
* Overriden to use a layout that has a fixed width arrow button.
*
* @return an instance of a layout manager
*/
@Override
protected LayoutManager createLayoutManager() {
return new PlasticComboBoxLayoutManager();
}
@Override
protected ComboPopup createPopup() {
return new PlasticComboPopup(comboBox);
}
/**
* Creates the default renderer that will be used in a non-editiable combo
* box. A default renderer will used only if a renderer has not been
* explicitly set with {@code setRenderer}.
*
* This method differs from the superclass implementation
* in that it uses an empty border with wider left and right margins
* of 2 pixels instead of 1.
*
* @return a {@code ListCellRender} used for the combo box
* @see javax.swing.JComboBox#setRenderer
*/
@Override
protected ListCellRenderer createRenderer() {
if (tableCellEditor) {
return super.createRenderer();
}
BasicComboBoxRenderer renderer = new BasicComboBoxRenderer.UIResource();
renderer.setBorder(UIManager.getBorder("ComboBox.rendererBorder"));
return renderer;
}
/**
* The minumum size is the size of the display area plus insets plus the button.
*/
@Override
public Dimension getMinimumSize(JComponent c) {
if (!isMinimumSizeDirty) {
return new Dimension(cachedMinimumSize);
}
Dimension size = getDisplaySize();
Insets insets = getInsets();
size.height += insets.top + insets.bottom;
if (comboBox.isEditable()) {
Insets editorBorderInsets = UIManager.getInsets("ComboBox.editorBorderInsets");
size.width += editorBorderInsets.left + editorBorderInsets.right;
//size.height += editorBorderInsets.top + editorBorderInsets.bottom;
// The combo editor benefits from extra space for the caret.
// To make editable and non-editable equally wide,
// we always add 1 pixel.
size.width += 1;
} else if (arrowButton != null) {
Insets arrowButtonInsets = arrowButton.getInsets();
size.width += arrowButtonInsets.left;
}
int buttonWidth = getEditableButtonWidth();
size.width += insets.left + insets.right + buttonWidth;
// Honor corrections made in #paintCurrentValue
ListCellRenderer renderer = comboBox.getRenderer();
if (renderer instanceof JComponent) {
JComponent component = (JComponent) renderer;
Insets rendererInsets = component.getInsets();
Insets editorInsets = UIManager.getInsets("ComboBox.editorInsets");
int offsetLeft = Math.max(0, editorInsets.left - rendererInsets.left);
int offsetRight = Math.max(0, editorInsets.right - rendererInsets.right);
// int offsetTop = Math.max(0, editorInsets.top - rendererInsets.top);
// int offsetBottom = Math.max(0, editorInsets.bottom - rendererInsets.bottom);
size.width += offsetLeft + offsetRight;
//size.height += offsetTop + offsetBottom;
}
// The height is oriented on the JTextField height
Dimension textFieldSize = PHANTOM.getMinimumSize();
size.height = Math.max(textFieldSize.height, size.height);
cachedMinimumSize.setSize(size.width, size.height);
isMinimumSizeDirty = false;
return new Dimension(size);
}
/**
* Delegates to #getMinimumSize(Component).
* Overridden to return the same result in JDK 1.5 as in JDK 1.4.
*/
@Override
public Dimension getPreferredSize(JComponent c) {
return getMinimumSize(c);
}
/**
* Returns the area that is reserved for drawing the currently selected item.
*/
@Override
protected Rectangle rectangleForCurrentValue() {
int width = comboBox.getWidth();
int height = comboBox.getHeight();
Insets insets = getInsets();
int buttonWidth = getEditableButtonWidth();
if (arrowButton != null) {
buttonWidth = arrowButton.getWidth();
}
if (comboBox.getComponentOrientation().isLeftToRight()) {
return new Rectangle(
insets.left,
insets.top,
width - (insets.left + insets.right + buttonWidth),
height - (insets.top + insets.bottom));
}
return new Rectangle(
insets.left + buttonWidth,
insets.top ,
width - (insets.left + insets.right + buttonWidth),
height - (insets.top + insets.bottom));
}
// Painting ***************************************************************
@Override
public void update(Graphics g, JComponent c) {
if (c.isOpaque()) {
g.setColor(c.getBackground());
g.fillRect(0, 0, c.getWidth(), c.getHeight());
if (isToolBarComboBox(c)) {
c.setOpaque(false);
} }
paint(g, c);
}
/**
* Checks and answers if this combo is in a tool bar.
*
* @param c the component to check
* @return true if in tool bar, false otherwise
*/
protected boolean isToolBarComboBox(JComponent c) {
Container parent = c.getParent();
return parent != null
&& (parent instanceof JToolBar
|| parent.getParent() instanceof JToolBar);
}
// Helper Code ************************************************************
/**
* Computes and returns the width of the arrow button in editable state.
* The perceived width shall be equal to the width of a scroll bar.
* Therefore we subtract a pixel that is perceived as part of the
* arrow button but that is painted by the editor's border.
*
* @return the width of the arrow button in editable state
*/
static int getEditableButtonWidth() {
return UIManager.getInt("ScrollBar.width") - 1;
}
/**
* Checks and answers if this UI's combo has a client property
* that indicates that the combo is used as a table cell editor.
*
* @return {@code true} if the table cell editor client property
* is set to {@code Boolean.TRUE}, {@code false} otherwise
*/
private boolean isTableCellEditor() {
return Boolean.TRUE.equals(comboBox.getClientProperty(CELL_EDITOR_KEY));
}
// Helper Classes *********************************************************
/**
* This layout manager handles the 'standard' layout of combo boxes.
* It puts the arrow button to the right and the editor to the left.
* If there is no editor it still keeps the arrow button to the right.
*
* Overriden to use a fixed arrow button width.
*/
private final class PlasticComboBoxLayoutManager
extends MetalComboBoxUI.MetalComboBoxLayoutManager {
@Override
public void layoutContainer(Container parent) {
JComboBox cb = (JComboBox) parent;
// Use superclass behavior if the combobox is not editable.
if (!cb.isEditable()) {
super.layoutContainer(parent);
return;
}
int width = cb.getWidth();
int height = cb.getHeight();
Insets insets = getInsets();
int buttonWidth = getEditableButtonWidth();
int buttonHeight = height - (insets.top + insets.bottom);
if (arrowButton != null) {
if (cb.getComponentOrientation().isLeftToRight()) {
arrowButton.setBounds(
width - (insets.right + buttonWidth),
insets.top,
buttonWidth,
buttonHeight);
} else {
arrowButton.setBounds(
insets.left,
insets.top,
buttonWidth,
buttonHeight);
}
}
if (editor != null) {
editor.setBounds(rectangleForCurrentValue());
}
}
}
// Required if we have a combobox button that does not extend MetalComboBoxButton
@Override
public PropertyChangeListener createPropertyChangeListener() {
return new PlasticPropertyChangeListener();
}
/**
* Overriden to use PlasticComboBoxButton instead of a MetalComboBoxButton.
* Required if we have a combobox button that does not extend MetalComboBoxButton
*/
private final class PlasticPropertyChangeListener
extends BasicComboBoxUI.PropertyChangeHandler {
@Override
public void propertyChange(PropertyChangeEvent e) {
super.propertyChange(e);
String propertyName = e.getPropertyName();
if (propertyName.equals("editable")) {
PlasticComboBoxButton button =
(PlasticComboBoxButton) arrowButton;
button.setIconOnly(comboBox.isEditable());
comboBox.repaint();
} else if (propertyName.equals("background")) {
Color color = (Color) e.getNewValue();
arrowButton.setBackground(color);
listBox.setBackground(color);
} else if (propertyName.equals("foreground")) {
Color color = (Color) e.getNewValue();
arrowButton.setForeground(color);
listBox.setForeground(color);
}
}
}
/**
* Differs from the BasicComboPopup in that it uses the standard
* popmenu border and honors an optional popup prototype display value.
*/
private static final class PlasticComboPopup extends BasicComboPopup {
private PlasticComboPopup(JComboBox combo) {
super(combo);
}
/**
* Configures the list created by #createList().
*/
@Override
protected void configureList() {
super.configureList();
list.setForeground(UIManager.getColor("MenuItem.foreground"));
list.setBackground(UIManager.getColor("MenuItem.background"));
}
/**
* Configures the JScrollPane created by #createScroller().
*/
@Override
protected void configureScroller() {
super.configureScroller();
scroller.getVerticalScrollBar().putClientProperty(
MetalScrollBarUI.FREE_STANDING_PROP,
Boolean.FALSE);
}
/**
* Calculates 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.
*
* In addition to the superclass behavior, this class offers
* to use the combo's popup prototype display value to compute
* the popup menu width. This is an optional feature of the
* JGoodies Plastic L&fs implemented via a client property key.
*
* If a prototype is set, the popup width is the maximum of the
* combobox width and the prototype based popup width.
* For the latter the renderer is used to render the prototype.
* The prototype based popup width is the prototype's width
* plus the scrollbar width - if any. The scrollbar test checks
* if there are more items than the combo's maximum row count.
*
* @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
*
* @see Options#COMBO_POPUP_PROTOTYPE_DISPLAY_VALUE_KEY
* @see JComboBox#getMaximumRowCount()
*/
@Override
protected Rectangle computePopupBounds(int px, int py, int pw, int ph) {
Rectangle defaultBounds = super.computePopupBounds(px, py, pw, ph);
Object popupPrototypeDisplayValue = comboBox.getClientProperty(
Options.COMBO_POPUP_PROTOTYPE_DISPLAY_VALUE_KEY);
if (popupPrototypeDisplayValue == null) {
return defaultBounds;
}
ListCellRenderer renderer = list.getCellRenderer();
Component c = renderer.getListCellRendererComponent(
list, popupPrototypeDisplayValue, -1, true, true);
pw = c.getPreferredSize().width;
boolean hasVerticalScrollBar =
comboBox.getItemCount() > comboBox.getMaximumRowCount();
if (hasVerticalScrollBar) {
// Add the scrollbar width.
JScrollBar verticalBar = scroller.getVerticalScrollBar();
pw += verticalBar.getPreferredSize().width;
}
Rectangle prototypeBasedBounds = super.computePopupBounds(px, py, pw, ph);
return prototypeBasedBounds.width > defaultBounds.width
? prototypeBasedBounds
: defaultBounds;
}
}
// Handling Combo Changes *************************************************
/**
* Listens to changes in the table cell editor client property
* and updates the default editor - if any - to use the correct
* insets for this case.
*/
private final class TableCellEditorPropertyChangeHandler implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
tableCellEditor = isTableCellEditor();
if (comboBox.getRenderer() == null || comboBox.getRenderer() instanceof UIResource) {
comboBox.setRenderer(createRenderer());
}
if (comboBox.getEditor() == null || comboBox.getEditor() instanceof UIResource) {
comboBox.setEditor(createEditor());
}
}
}
}