001: /*
002: * Copyright (c) 2001-2007 JGoodies Karsten Lentzsch. All Rights Reserved.
003: *
004: * Redistribution and use in source and binary forms, with or without
005: * modification, are permitted provided that the following conditions are met:
006: *
007: * o Redistributions of source code must retain the above copyright notice,
008: * this list of conditions and the following disclaimer.
009: *
010: * o Redistributions in binary form must reproduce the above copyright notice,
011: * this list of conditions and the following disclaimer in the documentation
012: * and/or other materials provided with the distribution.
013: *
014: * o Neither the name of JGoodies Karsten Lentzsch nor the names of
015: * its contributors may be used to endorse or promote products derived
016: * from this software without specific prior written permission.
017: *
018: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
019: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
020: * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
021: * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
022: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
023: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
024: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
025: * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
026: * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
027: * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
028: * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029: */
030:
031: package com.jgoodies.looks.plastic;
032:
033: import java.awt.*;
034: import java.beans.PropertyChangeEvent;
035: import java.beans.PropertyChangeListener;
036:
037: import javax.swing.*;
038: import javax.swing.plaf.ComponentUI;
039: import javax.swing.plaf.TextUI;
040: import javax.swing.plaf.UIResource;
041: import javax.swing.plaf.basic.BasicComboBoxRenderer;
042: import javax.swing.plaf.basic.BasicComboBoxUI;
043: import javax.swing.plaf.basic.BasicComboPopup;
044: import javax.swing.plaf.basic.ComboPopup;
045: import javax.swing.plaf.metal.MetalComboBoxUI;
046: import javax.swing.plaf.metal.MetalScrollBarUI;
047: import javax.swing.plaf.metal.MetalTextFieldUI;
048:
049: import com.jgoodies.looks.Options;
050:
051: /**
052: * The JGoodies Plastic Look and Feel implementation of <code>ComboBoxUI</code>.
053: * Has the same height as text fields - unless you change the renderer.<p>
054: *
055: * Also, this class offers to use the combo's popup prototype display value
056: * to compute the popup menu width. This is an optional feature of
057: * the JGoodies Plastic L&fs implemented via a client property key.
058: *
059: * @author Karsten Lentzsch
060: * @version $Revision: 1.13 $
061: *
062: * @see Options#COMBO_POPUP_PROTOTYPE_DISPLAY_VALUE_KEY
063: */
064: public class PlasticComboBoxUI extends MetalComboBoxUI {
065:
066: static final String CELL_EDITOR_KEY = "JComboBox.isTableCellEditor";
067:
068: /**
069: * Used to determine the minimum height of a text field,
070: * which in turn is used to answer the combobox's minimum height.
071: */
072: private static final JTextField PHANTOM = new JTextField("Phantom");
073:
074: /**
075: * Different Plastic L&fs may need different phantom UIs.
076: * Therefore we store the LookAndFeel class and update the
077: * phantom UI whenever the Look&Feel changes.
078: */
079: private static Class phantomLafClass;
080:
081: private boolean tableCellEditor;
082: private PropertyChangeListener propertyChangeListener;
083:
084: // ************************************************************************
085:
086: public static ComponentUI createUI(JComponent b) {
087: ensurePhantomHasPlasticUI();
088: return new PlasticComboBoxUI();
089: }
090:
091: /**
092: * Ensures that the phantom text field has a Plastic text field UI.
093: */
094: private static void ensurePhantomHasPlasticUI() {
095: TextUI ui = PHANTOM.getUI();
096: Class lafClass = UIManager.getLookAndFeel().getClass();
097: if ((phantomLafClass != lafClass)
098: || !(ui instanceof MetalTextFieldUI)) {
099: phantomLafClass = lafClass;
100: PHANTOM.updateUI();
101: }
102: }
103:
104: // ************************************************************************
105:
106: public void installUI(JComponent c) {
107: super .installUI(c);
108: tableCellEditor = isTableCellEditor();
109: }
110:
111: protected void installListeners() {
112: super .installListeners();
113: propertyChangeListener = new TableCellEditorPropertyChangeHandler();
114: comboBox.addPropertyChangeListener(CELL_EDITOR_KEY,
115: propertyChangeListener);
116: }
117:
118: protected void uninstallListeners() {
119: super .uninstallListeners();
120: comboBox.removePropertyChangeListener(CELL_EDITOR_KEY,
121: propertyChangeListener);
122: propertyChangeListener = null;
123: }
124:
125: // Overridden Superclass Configuration ************************************
126:
127: /**
128: * Creates and answers the arrow button that is to be used in the combo box.<p>
129: *
130: * Overridden to use a button that can have a pseudo 3D effect.
131: */
132: protected JButton createArrowButton() {
133: return new PlasticComboBoxButton(comboBox, PlasticIconFactory
134: .getComboBoxButtonIcon(), comboBox.isEditable(),
135: currentValuePane, listBox);
136: }
137:
138: /**
139: * Creates the editor that is to be used in editable combo boxes.
140: * This method only gets called if a custom editor has not already
141: * been installed in the JComboBox.
142: */
143: protected ComboBoxEditor createEditor() {
144: return new PlasticComboBoxEditor.UIResource(tableCellEditor);
145: }
146:
147: /**
148: * Creates a layout manager for managing the components which
149: * make up the combo box.<p>
150: *
151: * Overriden to use a layout that has a fixed width arrow button.
152: *
153: * @return an instance of a layout manager
154: */
155: protected LayoutManager createLayoutManager() {
156: return new PlasticComboBoxLayoutManager();
157: }
158:
159: protected ComboPopup createPopup() {
160: return new PlasticComboPopup(comboBox);
161: }
162:
163: /**
164: * Creates the default renderer that will be used in a non-editiable combo
165: * box. A default renderer will used only if a renderer has not been
166: * explicitly set with <code>setRenderer</code>.<p>
167: *
168: * This method differs from the superclass implementation
169: * in that it uses an empty border with wider left and right margins
170: * of 2 pixels instead of 1.
171: *
172: * @return a <code>ListCellRender</code> used for the combo box
173: * @see javax.swing.JComboBox#setRenderer
174: */
175: protected ListCellRenderer createRenderer() {
176: if (tableCellEditor) {
177: return super .createRenderer();
178: }
179: BasicComboBoxRenderer renderer = new BasicComboBoxRenderer.UIResource();
180: renderer.setBorder(UIManager
181: .getBorder("ComboBox.rendererBorder"));
182: return renderer;
183: }
184:
185: /**
186: * The minumum size is the size of the display area plus insets plus the button.
187: */
188: public Dimension getMinimumSize(JComponent c) {
189: if (!isMinimumSizeDirty) {
190: return new Dimension(cachedMinimumSize);
191: }
192: Dimension size = getDisplaySize();
193: Insets insets = getInsets();
194: size.height += insets.top + insets.bottom;
195: if (comboBox.isEditable()) {
196: Insets editorBorderInsets = UIManager
197: .getInsets("ComboBox.editorBorderInsets");
198: size.width += editorBorderInsets.left
199: + editorBorderInsets.right;
200: //size.height += editorBorderInsets.top + editorBorderInsets.bottom;
201: // The combo editor benefits from extra space for the caret.
202: // To make editable and non-editable equally wide,
203: // we always add 1 pixel.
204: size.width += 1;
205: } else if (arrowButton != null) {
206: Insets arrowButtonInsets = arrowButton.getInsets();
207: size.width += arrowButtonInsets.left;
208: }
209: int buttonWidth = getEditableButtonWidth();
210: size.width += insets.left + insets.right + buttonWidth;
211:
212: // Honor corrections made in #paintCurrentValue
213: ListCellRenderer renderer = comboBox.getRenderer();
214: if (renderer instanceof JComponent) {
215: JComponent component = (JComponent) renderer;
216: Insets rendererInsets = component.getInsets();
217: Insets editorInsets = UIManager
218: .getInsets("ComboBox.editorInsets");
219: int offsetLeft = Math.max(0, editorInsets.left
220: - rendererInsets.left);
221: int offsetRight = Math.max(0, editorInsets.right
222: - rendererInsets.right);
223: // int offsetTop = Math.max(0, editorInsets.top - rendererInsets.top);
224: // int offsetBottom = Math.max(0, editorInsets.bottom - rendererInsets.bottom);
225: size.width += offsetLeft + offsetRight;
226: //size.height += offsetTop + offsetBottom;
227: }
228:
229: // The height is oriented on the JTextField height
230: Dimension textFieldSize = PHANTOM.getMinimumSize();
231: size.height = Math.max(textFieldSize.height, size.height);
232:
233: cachedMinimumSize.setSize(size.width, size.height);
234: isMinimumSizeDirty = false;
235:
236: return new Dimension(size);
237: }
238:
239: /**
240: * Delegates to #getMinimumSize(Component).
241: * Overridden to return the same result in JDK 1.5 as in JDK 1.4.
242: */
243: public Dimension getPreferredSize(JComponent c) {
244: return getMinimumSize(c);
245: }
246:
247: /**
248: * Returns the area that is reserved for drawing the currently selected item.
249: */
250: protected Rectangle rectangleForCurrentValue() {
251: int width = comboBox.getWidth();
252: int height = comboBox.getHeight();
253: Insets insets = getInsets();
254: int buttonWidth = getEditableButtonWidth();
255: if (arrowButton != null) {
256: buttonWidth = arrowButton.getWidth();
257: }
258: if (comboBox.getComponentOrientation().isLeftToRight()) {
259: return new Rectangle(insets.left, insets.top, width
260: - (insets.left + insets.right + buttonWidth),
261: height - (insets.top + insets.bottom));
262: } else {
263: return new Rectangle(insets.left + buttonWidth, insets.top,
264: width - (insets.left + insets.right + buttonWidth),
265: height - (insets.top + insets.bottom));
266: }
267: }
268:
269: // Painting ***************************************************************
270:
271: public void update(Graphics g, JComponent c) {
272: if (c.isOpaque()) {
273: g.setColor(c.getBackground());
274: g.fillRect(0, 0, c.getWidth(), c.getHeight());
275: if (isToolBarComboBox(c)) {
276: c.setOpaque(false);
277: }
278: }
279: paint(g, c);
280: }
281:
282: /**
283: * Checks and answers if this combo is in a tool bar.
284: *
285: * @param c the component to check
286: * @return true if in tool bar, false otherwise
287: */
288: protected boolean isToolBarComboBox(JComponent c) {
289: Container parent = c.getParent();
290: return parent != null
291: && (parent instanceof JToolBar || parent.getParent() instanceof JToolBar);
292: }
293:
294: // Helper Code ************************************************************
295:
296: /**
297: * Computes and returns the width of the arrow button in editable state.
298: * The perceived width shall be equal to the width of a scroll bar.
299: * Therefore we subtract a pixel that is perceived as part of the
300: * arrow button but that is painted by the editor's border.
301: *
302: * @return the width of the arrow button in editable state
303: */
304: static int getEditableButtonWidth() {
305: return UIManager.getInt("ScrollBar.width") - 1;
306: }
307:
308: /**
309: * Checks and answers if this UI's combo has a client property
310: * that indicates that the combo is used as a table cell editor.
311: *
312: * @return <code>true</code> if the table cell editor client property
313: * is set to <code>Boolean.TRUE</code>, <code>false</code> otherwise
314: */
315: private boolean isTableCellEditor() {
316: return Boolean.TRUE.equals(comboBox
317: .getClientProperty(CELL_EDITOR_KEY));
318: }
319:
320: // Helper Classes *********************************************************
321:
322: /**
323: * This layout manager handles the 'standard' layout of combo boxes.
324: * It puts the arrow button to the right and the editor to the left.
325: * If there is no editor it still keeps the arrow button to the right.
326: *
327: * Overriden to use a fixed arrow button width.
328: */
329: private final class PlasticComboBoxLayoutManager extends
330: MetalComboBoxUI.MetalComboBoxLayoutManager {
331:
332: public void layoutContainer(Container parent) {
333: JComboBox cb = (JComboBox) parent;
334:
335: // Use superclass behavior if the combobox is not editable.
336: if (!cb.isEditable()) {
337: super .layoutContainer(parent);
338: return;
339: }
340:
341: int width = cb.getWidth();
342: int height = cb.getHeight();
343:
344: Insets insets = getInsets();
345: int buttonWidth = getEditableButtonWidth();
346: int buttonHeight = height - (insets.top + insets.bottom);
347:
348: if (arrowButton != null) {
349: if (cb.getComponentOrientation().isLeftToRight()) {
350: arrowButton.setBounds(width
351: - (insets.right + buttonWidth), insets.top,
352: buttonWidth, buttonHeight);
353: } else {
354: arrowButton.setBounds(insets.left, insets.top,
355: buttonWidth, buttonHeight);
356: }
357: }
358: if (editor != null) {
359: editor.setBounds(rectangleForCurrentValue());
360: }
361: }
362: }
363:
364: // Required if we have a combobox button that does not extend MetalComboBoxButton
365: public PropertyChangeListener createPropertyChangeListener() {
366: return new PlasticPropertyChangeListener();
367: }
368:
369: /**
370: * Overriden to use PlasticComboBoxButton instead of a MetalComboBoxButton.
371: * Required if we have a combobox button that does not extend MetalComboBoxButton
372: */
373: private final class PlasticPropertyChangeListener extends
374: BasicComboBoxUI.PropertyChangeHandler {
375:
376: public void propertyChange(PropertyChangeEvent e) {
377: super .propertyChange(e);
378: String propertyName = e.getPropertyName();
379:
380: if (propertyName.equals("editable")) {
381: PlasticComboBoxButton button = (PlasticComboBoxButton) arrowButton;
382: button.setIconOnly(comboBox.isEditable());
383: comboBox.repaint();
384: } else if (propertyName.equals("background")) {
385: Color color = (Color) e.getNewValue();
386: arrowButton.setBackground(color);
387: listBox.setBackground(color);
388:
389: } else if (propertyName.equals("foreground")) {
390: Color color = (Color) e.getNewValue();
391: arrowButton.setForeground(color);
392: listBox.setForeground(color);
393: }
394: }
395: }
396:
397: /**
398: * Differs from the BasicComboPopup in that it uses the standard
399: * popmenu border and honors an optional popup prototype display value.
400: */
401: private static final class PlasticComboPopup extends
402: BasicComboPopup {
403:
404: private PlasticComboPopup(JComboBox combo) {
405: super (combo);
406: }
407:
408: /**
409: * Configures the list created by #createList().
410: */
411: protected void configureList() {
412: super .configureList();
413: list.setForeground(UIManager
414: .getColor("MenuItem.foreground"));
415: list.setBackground(UIManager
416: .getColor("MenuItem.background"));
417: }
418:
419: /**
420: * Configures the JScrollPane created by #createScroller().
421: */
422: protected void configureScroller() {
423: super .configureScroller();
424: scroller.getVerticalScrollBar().putClientProperty(
425: MetalScrollBarUI.FREE_STANDING_PROP, Boolean.FALSE);
426: }
427:
428: /**
429: * Calculates the placement and size of the popup portion
430: * of the combo box based on the combo box location and
431: * the enclosing screen bounds. If no transformations are required,
432: * then the returned rectangle will have the same values
433: * as the parameters.<p>
434: *
435: * In addition to the superclass behavior, this class offers
436: * to use the combo's popup prototype display value to compute
437: * the popup menu width. This is an optional feature of the
438: * JGoodies Plastic L&fs implemented via a client property key.<p>
439: *
440: * If a prototype is set, the popup width is the maximum of the
441: * combobox width and the prototype based popup width.
442: * For the latter the renderer is used to render the prototype.
443: * The prototype based popup width is the prototype's width
444: * plus the scrollbar width - if any. The scrollbar test checks
445: * if there are more items than the combo's maximum row count.
446: *
447: * @param px starting x location
448: * @param py starting y location
449: * @param pw starting width
450: * @param ph starting height
451: * @return a rectangle which represents the placement and size of the popup
452: *
453: * @see Options#COMBO_POPUP_PROTOTYPE_DISPLAY_VALUE_KEY
454: * @see JComboBox#getMaximumRowCount()
455: */
456: protected Rectangle computePopupBounds(int px, int py, int pw,
457: int ph) {
458: Rectangle defaultBounds = super .computePopupBounds(px, py,
459: pw, ph);
460: Object popupPrototypeDisplayValue = comboBox
461: .getClientProperty(Options.COMBO_POPUP_PROTOTYPE_DISPLAY_VALUE_KEY);
462: if (popupPrototypeDisplayValue == null) {
463: return defaultBounds;
464: }
465:
466: ListCellRenderer renderer = list.getCellRenderer();
467: Component c = renderer.getListCellRendererComponent(list,
468: popupPrototypeDisplayValue, -1, true, true);
469: pw = c.getPreferredSize().width;
470: boolean hasVerticalScrollBar = comboBox.getItemCount() > comboBox
471: .getMaximumRowCount();
472: if (hasVerticalScrollBar) {
473: // Add the scrollbar width.
474: JScrollBar verticalBar = scroller
475: .getVerticalScrollBar();
476: pw += verticalBar.getPreferredSize().width;
477: }
478: Rectangle prototypeBasedBounds = super .computePopupBounds(
479: px, py, pw, ph);
480: return prototypeBasedBounds.width > defaultBounds.width ? prototypeBasedBounds
481: : defaultBounds;
482: }
483:
484: }
485:
486: // Handling Combo Changes *************************************************
487:
488: /**
489: * Listens to changes in the table cell editor client property
490: * and updates the default editor - if any - to use the correct
491: * insets for this case.
492: */
493: private final class TableCellEditorPropertyChangeHandler implements
494: PropertyChangeListener {
495: public void propertyChange(PropertyChangeEvent evt) {
496: tableCellEditor = isTableCellEditor();
497: if (comboBox.getRenderer() == null
498: || comboBox.getRenderer() instanceof UIResource) {
499: comboBox.setRenderer(createRenderer());
500: }
501: if (comboBox.getEditor() == null
502: || comboBox.getEditor() instanceof UIResource) {
503: comboBox.setEditor(createEditor());
504: }
505: }
506: }
507:
508: }
|