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.windows;
032:
033: import java.awt.*;
034: import java.beans.PropertyChangeEvent;
035: import java.beans.PropertyChangeListener;
036:
037: import javax.swing.*;
038: import javax.swing.border.Border;
039: import javax.swing.border.EmptyBorder;
040: import javax.swing.plaf.ComponentUI;
041: import javax.swing.plaf.UIResource;
042: import javax.swing.plaf.basic.BasicComboBoxRenderer;
043: import javax.swing.plaf.basic.BasicComboBoxUI;
044: import javax.swing.plaf.basic.BasicComboPopup;
045: import javax.swing.plaf.basic.ComboPopup;
046:
047: import com.jgoodies.looks.LookUtils;
048: import com.jgoodies.looks.Options;
049: import com.sun.java.swing.plaf.windows.WindowsTextFieldUI;
050:
051: /**
052: * The JGoodies Windows Look&Feel implementation of
053: * {@link javax.swing.plaf.ComboBoxUI}.
054: * Corrects the editor insets for editable combo boxes
055: * as well as the render insets for non-editable combos. And it has
056: * the same height as text fields - unless you change the renderer.<p>
057: *
058: * Also, this class offers to use the combo's popup prototype display value
059: * to compute the popup menu width. This is an optional feature of
060: * the JGoodies Windows L&f implemented via a client property key.
061: *
062: * @author Karsten Lentzsch
063: * @version $Revision: 1.19 $
064: *
065: * @see Options#COMBO_POPUP_PROTOTYPE_DISPLAY_VALUE_KEY
066: */
067: public class WindowsComboBoxUI extends
068: com.sun.java.swing.plaf.windows.WindowsComboBoxUI {
069:
070: private static final String CELL_EDITOR_KEY = "JComboBox.isTableCellEditor";
071:
072: /**
073: * Used to determine the minimum height of a text field,
074: * which in turn is used to answer the combobox's minimum height.
075: */
076: private static final JTextField PHANTOM = new JTextField("Phantom");
077:
078: private static final Insets EMPTY_INSETS = new Insets(0, 0, 0, 0);
079: private static final Border EMPTY_BORDER = new EmptyBorder(
080: EMPTY_INSETS);
081:
082: private boolean tableCellEditor;
083: private PropertyChangeListener propertyChangeListener;
084:
085: // ************************************************************************
086:
087: public static ComponentUI createUI(JComponent b) {
088: ensurePhantomHasWindowsUI();
089: return new WindowsComboBoxUI();
090: }
091:
092: /**
093: * Ensures that the phantom text field has a Windows text field UI.
094: */
095: private static void ensurePhantomHasWindowsUI() {
096: if (!(PHANTOM.getUI() instanceof WindowsTextFieldUI)) {
097: PHANTOM.updateUI();
098: }
099: }
100:
101: // ************************************************************************
102:
103: public void installUI(JComponent c) {
104: super .installUI(c);
105: tableCellEditor = isTableCellEditor();
106: }
107:
108: protected void installListeners() {
109: super .installListeners();
110: propertyChangeListener = new TableCellEditorPropertyChangeHandler();
111: comboBox.addPropertyChangeListener(CELL_EDITOR_KEY,
112: propertyChangeListener);
113: }
114:
115: protected void uninstallListeners() {
116: super .uninstallListeners();
117: comboBox.removePropertyChangeListener(CELL_EDITOR_KEY,
118: propertyChangeListener);
119: propertyChangeListener = null;
120: }
121:
122: /**
123: * Creates the arrow button that is to be used in the combo box.<p>
124: *
125: * Overridden to paint black triangles.
126: */
127: protected JButton createArrowButton() {
128: return LookUtils.IS_LAF_WINDOWS_XP_ENABLED ? super
129: .createArrowButton() : new WindowsArrowButton(
130: SwingConstants.SOUTH);
131: }
132:
133: /**
134: * Creates the editor that is to be used in editable combo boxes.
135: * This method only gets called if a custom editor has not already
136: * been installed in the JComboBox.
137: */
138: protected ComboBoxEditor createEditor() {
139: return new com.jgoodies.looks.windows.WindowsComboBoxEditor.UIResource(
140: tableCellEditor);
141: }
142:
143: /**
144: * Creates a layout manager for managing the components which
145: * make up the combo box.<p>
146: *
147: * Overriden to use a layout that has a fixed width arrow button.
148: *
149: * @return an instance of a layout manager
150: */
151: protected LayoutManager createLayoutManager() {
152: return new WindowsComboBoxLayoutManager();
153: }
154:
155: protected void configureEditor() {
156: super .configureEditor();
157: if (!comboBox.isEnabled()) {
158: editor.setBackground(UIManager
159: .getColor("ComboBox.disabledBackground"));
160: }
161: }
162:
163: /**
164: * Creates a ComboPopup that honors the optional combo popup display value
165: * that is used to compute the popup menu width.
166: */
167: protected ComboPopup createPopup() {
168: return new WindowsComboPopup(comboBox);
169: }
170:
171: /**
172: * Creates the default renderer that will be used in a non-editiable combo
173: * box. A default renderer will used only if a renderer has not been
174: * explicitly set with <code>setRenderer</code>.<p>
175: *
176: * This method differs from the superclass implementation in that
177: * it uses an empty border with the default left and right text insets,
178: * the same as used by a combo box editor.
179: *
180: * @return a <code>ListCellRender</code> used for the combo box
181: * @see javax.swing.JComboBox#setRenderer
182: */
183: protected ListCellRenderer createRenderer() {
184: if (tableCellEditor) {
185: return super .createRenderer();
186: }
187: BasicComboBoxRenderer renderer = new BasicComboBoxRenderer.UIResource();
188: renderer.setBorder(UIManager
189: .getBorder("ComboBox.rendererBorder"));
190: return renderer;
191: }
192:
193: /**
194: * The minumum size is the size of the display area plus insets plus the button.
195: */
196: public Dimension getMinimumSize(JComponent c) {
197: if (!isMinimumSizeDirty) {
198: return new Dimension(cachedMinimumSize);
199: }
200: Dimension size = getDisplaySize();
201: Insets insets = getInsets();
202: size.height += insets.top + insets.bottom;
203: int buttonWidth = getEditableButtonWidth();
204: size.width += insets.left + insets.right + buttonWidth;
205: // The combo editor benefits from extra space for the caret.
206: // To make editable and non-editable equally wide,
207: // we always add 1 pixel.
208: size.width += 1;
209:
210: // Honor corrections made in #paintCurrentValue
211: ListCellRenderer renderer = comboBox.getRenderer();
212: if (renderer instanceof JComponent) {
213: JComponent component = (JComponent) renderer;
214: Insets rendererInsets = component.getInsets();
215: Insets editorInsets = UIManager
216: .getInsets("ComboBox.editorInsets");
217: int offsetLeft = Math.max(0, editorInsets.left
218: - rendererInsets.left);
219: int offsetRight = Math.max(0, editorInsets.right
220: - rendererInsets.right);
221: // int offsetTop = Math.max(0, editorInsets.top - rendererInsets.top);
222: // int offsetBottom = Math.max(0, editorInsets.bottom - rendererInsets.bottom);
223: size.width += offsetLeft + offsetRight;
224: //size.height += offsetTop + offsetBottom;
225: }
226:
227: // The height is oriented on the JTextField height
228: Dimension textFieldSize = PHANTOM.getMinimumSize();
229: size.height = (LookUtils.IS_OS_WINDOWS_VISTA && !LookUtils.IS_LAF_WINDOWS_XP_ENABLED) ? textFieldSize.height
230: : Math.max(textFieldSize.height, size.height);
231:
232: cachedMinimumSize.setSize(size.width, size.height);
233: isMinimumSizeDirty = false;
234:
235: return new Dimension(size);
236: }
237:
238: /**
239: * Delegates to #getMinimumSize(Component).
240: * Overridden to return the same result in JDK 1.5 as in JDK 1.4.
241: */
242: public Dimension getPreferredSize(JComponent c) {
243: return getMinimumSize(c);
244: }
245:
246: /**
247: * Paints the currently selected item.
248: */
249: public void paintCurrentValue(Graphics g, Rectangle bounds,
250: boolean hasFocus) {
251: ListCellRenderer renderer = comboBox.getRenderer();
252: Component c;
253: boolean isVistaReadOnlyCombo = isVistaXPStyleReadOnlyCombo();
254:
255: if (hasFocus && !isPopupVisible(comboBox)) {
256: c = renderer.getListCellRendererComponent(listBox, comboBox
257: .getSelectedItem(), -1, true, false);
258: } else {
259: c = renderer.getListCellRendererComponent(listBox, comboBox
260: .getSelectedItem(), -1, false, false);
261: c.setBackground(UIManager.getColor("ComboBox.background"));
262: }
263: Border oldBorder = null;
264: Rectangle originalBounds = new Rectangle(bounds);
265: if ((c instanceof JComponent) && !tableCellEditor) {
266: JComponent component = (JComponent) c;
267: if (isRendererBorderRemovable(component)) {
268: oldBorder = component.getBorder();
269: component.setBorder(EMPTY_BORDER); //new WindowsBorders.DashedBorder(c.getForeground(), 1));
270: }
271: Insets rendererInsets = component.getInsets();
272: Insets editorInsets = UIManager
273: .getInsets("ComboBox.editorInsets");
274: int offsetLeft = Math.max(0, editorInsets.left
275: - rendererInsets.left);
276: int offsetRight = Math.max(0, editorInsets.right
277: - rendererInsets.right);
278: int offsetTop = Math.max(0, editorInsets.top
279: - rendererInsets.top);
280: int offsetBottom = Math.max(0, editorInsets.bottom
281: - rendererInsets.bottom);
282: bounds.x += offsetLeft;
283: bounds.y += offsetTop;
284: bounds.width -= offsetLeft + offsetRight - 1;
285: bounds.height -= offsetTop + offsetBottom;
286: }
287:
288: c.setFont(comboBox.getFont());
289: if (hasFocus && !isPopupVisible(comboBox)
290: && !isVistaReadOnlyCombo) {
291: c.setForeground(listBox.getSelectionForeground());
292: c.setBackground(listBox.getSelectionBackground());
293: } else {
294: if (comboBox.isEnabled()) {
295: c.setForeground(comboBox.getForeground());
296: c.setBackground(comboBox.getBackground());
297: } else {
298: c.setForeground(UIManager
299: .getColor("ComboBox.disabledForeground"));
300: c.setBackground(UIManager
301: .getColor("ComboBox.disabledBackground"));
302: }
303: }
304:
305: // Fix for 4238829: should lay out the JPanel.
306: boolean shouldValidate = c instanceof JPanel;
307:
308: Boolean oldOpaque = null;
309: if (isVistaReadOnlyCombo && (c instanceof JComponent)
310: && !(c instanceof DefaultListCellRenderer)) {
311: oldOpaque = Boolean.valueOf(c.isOpaque());
312: ((JComponent) c).setOpaque(false);
313: }
314: currentValuePane.paintComponent(g, c, comboBox, bounds.x,
315: bounds.y, bounds.width, bounds.height, shouldValidate);
316: if (hasFocus) {
317: Color oldColor = g.getColor();
318: g.setColor(comboBox.getForeground());
319: if (isVistaReadOnlyCombo) {
320: int width = originalBounds.width - 2;
321: if ((width % 2) == 0) {
322: width += 1;
323: }
324: WindowsUtils.drawRoundedDashedRect(g,
325: originalBounds.x + 1, originalBounds.y + 1,
326: width, originalBounds.height - 2);
327: } /*else {
328: BasicGraphicsUtils.drawDashedRect(g,
329: bounds.x, bounds.y, bounds.width, bounds.height);
330: }*/
331: g.setColor(oldColor);
332: }
333: if (oldOpaque != null) {
334: ((JComponent) c).setOpaque(oldOpaque.booleanValue());
335: }
336: if (oldBorder != null) {
337: ((JComponent) c).setBorder(oldBorder);
338: }
339: }
340:
341: /**
342: * Checks and answer whether the border of the given renderer component
343: * can be removed temporarily, so the combo's selection background will
344: * be consistent with the default renderer and native appearance.
345: * This test is invoked from <code>#paintCurrentValue</code>.<p>
346: *
347: * It is safe to remove an EmptyBorder if the component doesn't override
348: * <code>#update</code>, <code>#paint</code> and <code>#paintBorder</code>.
349: * Since we know the default renderer, we can remove its border.<p>
350: *
351: * Custom renderers may set a hint to make their border removable.
352: * To do so, set the client property "isBorderRemovable"
353: * to <code>Boolean.TRUE</code>. If this client property is set,
354: * its value will be returned. If it is not set, <code>true</code> is returned
355: * if and only if the component's border is an EmptyBorder.
356: *
357: * @param rendererComponent the renderer component to check
358: * @return true if the component's border can be removed, false if not
359: * @see #paintCurrentValue(Graphics, Rectangle, boolean)
360: */
361: protected boolean isRendererBorderRemovable(
362: JComponent rendererComponent) {
363: if (rendererComponent instanceof BasicComboBoxRenderer.UIResource)
364: return true;
365: Object hint = rendererComponent
366: .getClientProperty(Options.COMBO_RENDERER_IS_BORDER_REMOVABLE);
367: if (hint != null)
368: return Boolean.TRUE.equals(hint);
369: Border border = rendererComponent.getBorder();
370: return border instanceof EmptyBorder;
371: }
372:
373: private boolean isVistaXPStyleReadOnlyCombo() {
374: return LookUtils.IS_OS_WINDOWS_VISTA
375: && LookUtils.IS_LAF_WINDOWS_XP_ENABLED
376: && !comboBox.isEditable();
377: }
378:
379: /**
380: * Returns the area that is reserved for drawing the currently selected item.
381: */
382: protected Rectangle rectangleForCurrentValue() {
383: int width = comboBox.getWidth();
384: int height = comboBox.getHeight();
385: Insets insets = getInsets();
386: int buttonWidth = getEditableButtonWidth();
387: if (arrowButton != null) {
388: buttonWidth = arrowButton.getWidth();
389: }
390: if (comboBox.getComponentOrientation().isLeftToRight()) {
391: return new Rectangle(insets.left, insets.top, width
392: - (insets.left + insets.right + buttonWidth),
393: height - (insets.top + insets.bottom));
394: } else {
395: return new Rectangle(insets.left + buttonWidth, insets.top,
396: width - (insets.left + insets.right + buttonWidth),
397: height - (insets.top + insets.bottom));
398: }
399: }
400:
401: // Helper Code ************************************************************
402:
403: /**
404: * Computes and returns the width of the arrow button in editable state.
405: *
406: * @return the width of the arrow button in editable state
407: */
408: private int getEditableButtonWidth() {
409: return UIManager.getInt("ScrollBar.width");
410: }
411:
412: /**
413: * Checks and answers if this UI's combo has a client property
414: * that indicates that the combo is used as a table cell editor.
415: *
416: * @return <code>true</code> if the table cell editor client property
417: * is set to <code>Boolean.TRUE</code>, <code>false</code> otherwise
418: */
419: private boolean isTableCellEditor() {
420: return Boolean.TRUE.equals(comboBox
421: .getClientProperty(CELL_EDITOR_KEY));
422: }
423:
424: // Collaborator Classes ***************************************************
425:
426: /**
427: * This layout manager handles the 'standard' layout of combo boxes.
428: * It puts the arrow button to the right and the editor to the left.
429: * If there is no editor it still keeps the arrow button to the right.
430: *
431: * Overriden to use a fixed arrow button width.
432: */
433: private final class WindowsComboBoxLayoutManager extends
434: BasicComboBoxUI.ComboBoxLayoutManager {
435:
436: public void layoutContainer(Container parent) {
437: JComboBox cb = (JComboBox) parent;
438:
439: int width = cb.getWidth();
440: int height = cb.getHeight();
441:
442: Insets insets = getInsets();
443: int buttonWidth = getEditableButtonWidth();
444: int buttonHeight = height - (insets.top + insets.bottom);
445:
446: if (arrowButton != null) {
447: if (cb.getComponentOrientation().isLeftToRight()) {
448: arrowButton.setBounds(width
449: - (insets.right + buttonWidth), insets.top,
450: buttonWidth, buttonHeight);
451: } else {
452: arrowButton.setBounds(insets.left, insets.top,
453: buttonWidth, buttonHeight);
454: }
455: }
456: if (editor != null) {
457: editor.setBounds(rectangleForCurrentValue());
458: }
459: }
460:
461: }
462:
463: /**
464: * Differs from the BasicComboPopup in that it uses the standard
465: * popmenu border and honors an optional popup prototype display value.
466: */
467: private static final class WindowsComboPopup extends
468: BasicComboPopup {
469:
470: private WindowsComboPopup(JComboBox combo) {
471: super (combo);
472: }
473:
474: /**
475: * Calculates the placement and size of the popup portion
476: * of the combo box based on the combo box location and
477: * the enclosing screen bounds. If no transformations are required,
478: * then the returned rectangle will have the same values
479: * as the parameters.<p>
480: *
481: * In addition to the superclass behavior, this class offers
482: * to use the combo's popup prototype display value to compute
483: * the popup menu width. This is an optional feature of
484: * the JGoodies Windows L&f implemented via a client property key.<p>
485: *
486: * If a prototype is set, the popup width is the maximum of the
487: * combobox width and the prototype based popup width.
488: * For the latter the renderer is used to render the prototype.
489: * The prototype based popup width is the prototype's width
490: * plus the scrollbar width - if any. The scrollbar test checks
491: * if there are more items than the combo's maximum row count.
492: *
493: * @param px starting x location
494: * @param py starting y location
495: * @param pw starting width
496: * @param ph starting height
497: * @return a rectangle which represents the placement and size of the popup
498: *
499: * @see Options#COMBO_POPUP_PROTOTYPE_DISPLAY_VALUE_KEY
500: * @see JComboBox#getMaximumRowCount()
501: */
502: protected Rectangle computePopupBounds(int px, int py, int pw,
503: int ph) {
504: Rectangle defaultBounds = super .computePopupBounds(px, py,
505: pw, ph);
506: Object popupPrototypeDisplayValue = comboBox
507: .getClientProperty(Options.COMBO_POPUP_PROTOTYPE_DISPLAY_VALUE_KEY);
508: if (popupPrototypeDisplayValue == null) {
509: return defaultBounds;
510: }
511:
512: ListCellRenderer renderer = list.getCellRenderer();
513: Component c = renderer.getListCellRendererComponent(list,
514: popupPrototypeDisplayValue, -1, true, true);
515: pw = c.getPreferredSize().width;
516: boolean hasVerticalScrollBar = comboBox.getItemCount() > comboBox
517: .getMaximumRowCount();
518: if (hasVerticalScrollBar) {
519: // Add the scrollbar width.
520: JScrollBar verticalBar = scroller
521: .getVerticalScrollBar();
522: pw += verticalBar.getPreferredSize().width;
523: }
524: Rectangle prototypeBasedBounds = super .computePopupBounds(
525: px, py, pw, ph);
526: return prototypeBasedBounds.width > defaultBounds.width ? prototypeBasedBounds
527: : defaultBounds;
528: }
529:
530: }
531:
532: // Handling Combo Changes *************************************************
533:
534: /**
535: * Listens to changes in the table cell editor client property
536: * and updates the default editor - if any - to use the correct
537: * insets for this case.
538: */
539: private final class TableCellEditorPropertyChangeHandler implements
540: PropertyChangeListener {
541: public void propertyChange(PropertyChangeEvent evt) {
542: tableCellEditor = isTableCellEditor();
543: if (comboBox.getRenderer() == null
544: || comboBox.getRenderer() instanceof UIResource) {
545: comboBox.setRenderer(createRenderer());
546: }
547: if (comboBox.getEditor() == null
548: || comboBox.getEditor() instanceof UIResource) {
549: comboBox.setEditor(createEditor());
550: }
551: }
552: }
553:
554: }
|