1 /* 2 * Copyright (c) 2002, 2010, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javax.swing.plaf.synth; 27 28 import java.awt.*; 29 import java.awt.event.*; 30 import javax.swing.*; 31 import javax.swing.plaf.*; 32 import javax.swing.event.*; 33 import javax.swing.plaf.basic.*; 34 import java.beans.PropertyChangeListener; 35 import java.beans.PropertyChangeEvent; 36 37 /** 38 * Provides the Synth L&F UI delegate for 39 * {@link javax.swing.JComboBox}. 40 * 41 * @author Scott Violet 42 * @since 1.7 43 */ 44 public class SynthComboBoxUI extends BasicComboBoxUI implements 45 PropertyChangeListener, SynthUI { 46 private SynthStyle style; 47 private boolean useListColors; 48 49 /** 50 * Used to adjust the location and size of the popup. Very useful for 51 * situations such as we find in Nimbus where part of the border is used 52 * to paint the focus. In such cases, the border is empty space, and not 53 * part of the "visual" border, and in these cases, you'd like the popup 54 * to be adjusted such that it looks as if it were next to the visual border. 55 * You may want to use negative insets to get the right look. 56 */ 57 Insets popupInsets; 58 59 /** 60 * This flag may be set via UIDefaults. By default, it is false, to 61 * preserve backwards compatibility. If true, then the combo will 62 * "act as a button" when it is not editable. 63 */ 64 private boolean buttonWhenNotEditable; 65 66 /** 67 * A flag to indicate that the combo box and combo box button should 68 * remain in the PRESSED state while the combo popup is visible. 69 */ 70 private boolean pressedWhenPopupVisible; 71 72 /** 73 * When buttonWhenNotEditable is true, this field is used to help make 74 * the combo box appear and function as a button when the combo box is 75 * not editable. In such a state, you can click anywhere on the button 76 * to get it to open the popup. Also, anywhere you hover over the combo 77 * will cause the entire combo to go into "rollover" state, and anywhere 78 * you press will go into "pressed" state. This also keeps in sync the 79 * state of the combo and the arrowButton. 80 */ 81 private ButtonHandler buttonHandler; 82 83 /** 84 * Handler for repainting combo when editor component gains/looses focus 85 */ 86 private EditorFocusHandler editorFocusHandler; 87 88 /** 89 * If true, then the cell renderer will be forced to be non-opaque when 90 * used for rendering the selected item in the combo box (not in the list), 91 * and forced to opaque after rendering the selected value. 92 */ 93 private boolean forceOpaque = false; 94 95 /** 96 * Creates a new UI object for the given component. 97 * 98 * @param c component to create UI object for 99 * @return the UI object 100 */ 101 public static ComponentUI createUI(JComponent c) { 102 return new SynthComboBoxUI(); 103 } 104 105 /** 106 * @inheritDoc 107 * 108 * Overridden to ensure that ButtonHandler is created prior to any of 109 * the other installXXX methods, since several of them reference 110 * buttonHandler. 111 */ 112 @Override 113 public void installUI(JComponent c) { 114 buttonHandler = new ButtonHandler(); 115 super.installUI(c); 116 } 117 118 @Override 119 protected void installDefaults() { 120 updateStyle(comboBox); 121 } 122 123 private void updateStyle(JComboBox comboBox) { 124 SynthStyle oldStyle = style; 125 SynthContext context = getContext(comboBox, ENABLED); 126 127 style = SynthLookAndFeel.updateStyle(context, this); 128 if (style != oldStyle) { 129 padding = (Insets) style.get(context, "ComboBox.padding"); 130 popupInsets = (Insets)style.get(context, "ComboBox.popupInsets"); 131 useListColors = style.getBoolean(context, 132 "ComboBox.rendererUseListColors", true); 133 buttonWhenNotEditable = style.getBoolean(context, 134 "ComboBox.buttonWhenNotEditable", false); 135 pressedWhenPopupVisible = style.getBoolean(context, 136 "ComboBox.pressedWhenPopupVisible", false); 137 squareButton = style.getBoolean(context, 138 "ComboBox.squareButton", true); 139 140 if (oldStyle != null) { 141 uninstallKeyboardActions(); 142 installKeyboardActions(); 143 } 144 forceOpaque = style.getBoolean(context, 145 "ComboBox.forceOpaque", false); 146 } 147 context.dispose(); 148 149 if(listBox != null) { 150 SynthLookAndFeel.updateStyles(listBox); 151 } 152 } 153 154 /** 155 * @inheritDoc 156 */ 157 @Override 158 protected void installListeners() { 159 comboBox.addPropertyChangeListener(this); 160 comboBox.addMouseListener(buttonHandler); 161 editorFocusHandler = new EditorFocusHandler(comboBox); 162 super.installListeners(); 163 } 164 165 /** 166 * @inheritDoc 167 */ 168 @Override 169 public void uninstallUI(JComponent c) { 170 if (popup instanceof SynthComboPopup) { 171 ((SynthComboPopup)popup).removePopupMenuListener(buttonHandler); 172 } 173 super.uninstallUI(c); 174 buttonHandler = null; 175 } 176 177 /** 178 * @inheritDoc 179 */ 180 @Override 181 protected void uninstallDefaults() { 182 SynthContext context = getContext(comboBox, ENABLED); 183 184 style.uninstallDefaults(context); 185 context.dispose(); 186 style = null; 187 } 188 189 /** 190 * @inheritDoc 191 */ 192 @Override 193 protected void uninstallListeners() { 194 editorFocusHandler.unregister(); 195 comboBox.removePropertyChangeListener(this); 196 comboBox.removeMouseListener(buttonHandler); 197 buttonHandler.pressed = false; 198 buttonHandler.over = false; 199 super.uninstallListeners(); 200 } 201 202 /** 203 * @inheritDoc 204 */ 205 @Override 206 public SynthContext getContext(JComponent c) { 207 return getContext(c, getComponentState(c)); 208 } 209 210 private SynthContext getContext(JComponent c, int state) { 211 return SynthContext.getContext(SynthContext.class, c, 212 SynthLookAndFeel.getRegion(c), style, state); 213 } 214 215 private int getComponentState(JComponent c) { 216 // currently we have a broken situation where if a developer 217 // takes the border from a JComboBox and sets it on a JTextField 218 // then the codepath will eventually lead back to this method 219 // but pass in a JTextField instead of JComboBox! In case this 220 // happens, we just return the normal synth state for the component 221 // instead of doing anything special 222 if (!(c instanceof JComboBox)) return SynthLookAndFeel.getComponentState(c); 223 224 JComboBox box = (JComboBox)c; 225 if (shouldActLikeButton()) { 226 int state = ENABLED; 227 if ((!c.isEnabled())) { 228 state = DISABLED; 229 } 230 if (buttonHandler.isPressed()) { 231 state |= PRESSED; 232 } 233 if (buttonHandler.isRollover()) { 234 state |= MOUSE_OVER; 235 } 236 if (box.isFocusOwner()) { 237 state |= FOCUSED; 238 } 239 return state; 240 } else { 241 // for editable combos the editor component has the focus not the 242 // combo box its self, so we should make the combo paint focused 243 // when its editor has focus 244 int basicState = SynthLookAndFeel.getComponentState(c); 245 if (box.isEditable() && 246 box.getEditor().getEditorComponent().isFocusOwner()) { 247 basicState |= FOCUSED; 248 } 249 return basicState; 250 } 251 } 252 253 /** 254 * @inheritDoc 255 */ 256 @Override 257 protected ComboPopup createPopup() { 258 SynthComboPopup p = new SynthComboPopup(comboBox); 259 p.addPopupMenuListener(buttonHandler); 260 return p; 261 } 262 263 /** 264 * @inheritDoc 265 */ 266 @Override 267 protected ListCellRenderer createRenderer() { 268 return new SynthComboBoxRenderer(); 269 } 270 271 /** 272 * @inheritDoc 273 */ 274 @Override 275 protected ComboBoxEditor createEditor() { 276 return new SynthComboBoxEditor(); 277 } 278 279 // 280 // end UI Initialization 281 //====================== 282 283 /** 284 * @inheritDoc 285 */ 286 @Override 287 public void propertyChange(PropertyChangeEvent e) { 288 if (SynthLookAndFeel.shouldUpdateStyle(e)) { 289 updateStyle(comboBox); 290 } 291 } 292 293 /** 294 * @inheritDoc 295 */ 296 @Override 297 protected JButton createArrowButton() { 298 SynthArrowButton button = new SynthArrowButton(SwingConstants.SOUTH); 299 button.setName("ComboBox.arrowButton"); 300 button.setModel(buttonHandler); 301 return button; 302 } 303 304 //================================= 305 // begin ComponentUI Implementation 306 307 /** 308 * Notifies this UI delegate to repaint the specified component. 309 * This method paints the component background, then calls 310 * the {@link #paint(SynthContext,Graphics)} method. 311 * 312 * <p>In general, this method does not need to be overridden by subclasses. 313 * All Look and Feel rendering code should reside in the {@code paint} method. 314 * 315 * @param g the {@code Graphics} object used for painting 316 * @param c the component being painted 317 * @see #paint(SynthContext,Graphics) 318 */ 319 @Override 320 public void update(Graphics g, JComponent c) { 321 SynthContext context = getContext(c); 322 323 SynthLookAndFeel.update(context, g); 324 context.getPainter().paintComboBoxBackground(context, g, 0, 0, 325 c.getWidth(), c.getHeight()); 326 paint(context, g); 327 context.dispose(); 328 } 329 330 /** 331 * Paints the specified component according to the Look and Feel. 332 * <p>This method is not used by Synth Look and Feel. 333 * Painting is handled by the {@link #paint(SynthContext,Graphics)} method. 334 * 335 * @param g the {@code Graphics} object used for painting 336 * @param c the component being painted 337 * @see #paint(SynthContext,Graphics) 338 */ 339 @Override 340 public void paint(Graphics g, JComponent c) { 341 SynthContext context = getContext(c); 342 343 paint(context, g); 344 context.dispose(); 345 } 346 347 /** 348 * Paints the specified component. 349 * 350 * @param context context for the component being painted 351 * @param g the {@code Graphics} object used for painting 352 * @see #update(Graphics,JComponent) 353 */ 354 protected void paint(SynthContext context, Graphics g) { 355 hasFocus = comboBox.hasFocus(); 356 if ( !comboBox.isEditable() ) { 357 Rectangle r = rectangleForCurrentValue(); 358 paintCurrentValue(g,r,hasFocus); 359 } 360 } 361 362 /** 363 * @inheritDoc 364 */ 365 @Override 366 public void paintBorder(SynthContext context, Graphics g, int x, 367 int y, int w, int h) { 368 context.getPainter().paintComboBoxBorder(context, g, x, y, w, h); 369 } 370 371 /** 372 * Paints the currently selected item. 373 */ 374 @Override 375 public void paintCurrentValue(Graphics g,Rectangle bounds,boolean hasFocus) { 376 ListCellRenderer renderer = comboBox.getRenderer(); 377 Component c; 378 379 c = renderer.getListCellRendererComponent( 380 listBox, comboBox.getSelectedItem(), -1, false, false ); 381 382 // Fix for 4238829: should lay out the JPanel. 383 boolean shouldValidate = false; 384 if (c instanceof JPanel) { 385 shouldValidate = true; 386 } 387 388 if (c instanceof UIResource) { 389 c.setName("ComboBox.renderer"); 390 } 391 392 boolean force = forceOpaque && c instanceof JComponent; 393 if (force) { 394 ((JComponent)c).setOpaque(false); 395 } 396 397 int x = bounds.x, y = bounds.y, w = bounds.width, h = bounds.height; 398 if (padding != null) { 399 x = bounds.x + padding.left; 400 y = bounds.y + padding.top; 401 w = bounds.width - (padding.left + padding.right); 402 h = bounds.height - (padding.top + padding.bottom); 403 } 404 405 currentValuePane.paintComponent(g, c, comboBox, x, y, w, h, shouldValidate); 406 407 if (force) { 408 ((JComponent)c).setOpaque(true); 409 } 410 } 411 412 /** 413 * @return true if this combo box should act as one big button. Typically 414 * only happens when buttonWhenNotEditable is true, and comboBox.isEditable 415 * is false. 416 */ 417 private boolean shouldActLikeButton() { 418 return buttonWhenNotEditable && !comboBox.isEditable(); 419 } 420 421 /** 422 * Returns the default size of an empty display area of the combo box using 423 * the current renderer and font. 424 * 425 * This method was overridden to use SynthComboBoxRenderer instead of 426 * DefaultListCellRenderer as the default renderer when calculating the 427 * size of the combo box. This is used in the case of the combo not having 428 * any data. 429 * 430 * @return the size of an empty display area 431 * @see #getDisplaySize 432 */ 433 @Override 434 protected Dimension getDefaultSize() { 435 SynthComboBoxRenderer r = new SynthComboBoxRenderer(); 436 Dimension d = getSizeForComponent(r.getListCellRendererComponent(listBox, " ", -1, false, false)); 437 return new Dimension(d.width, d.height); 438 } 439 440 /** 441 * From BasicComboBoxRenderer v 1.18. 442 * 443 * Be aware that SynthFileChooserUIImpl relies on the fact that the default 444 * renderer installed on a Synth combo box is a JLabel. If this is changed, 445 * then an assert will fail in SynthFileChooserUIImpl 446 */ 447 private class SynthComboBoxRenderer extends JLabel implements ListCellRenderer, UIResource { 448 public SynthComboBoxRenderer() { 449 super(); 450 setName("ComboBox.renderer"); 451 setText(" "); 452 } 453 454 @Override 455 public Component getListCellRendererComponent(JList list, Object value, 456 int index, boolean isSelected, boolean cellHasFocus) { 457 setName("ComboBox.listRenderer"); 458 SynthLookAndFeel.resetSelectedUI(); 459 if (isSelected) { 460 setBackground(list.getSelectionBackground()); 461 setForeground(list.getSelectionForeground()); 462 if (!useListColors) { 463 SynthLookAndFeel.setSelectedUI( 464 (SynthLabelUI)SynthLookAndFeel.getUIOfType(getUI(), 465 SynthLabelUI.class), isSelected, cellHasFocus, 466 list.isEnabled(), false); 467 } 468 } else { 469 setBackground(list.getBackground()); 470 setForeground(list.getForeground()); 471 } 472 473 setFont(list.getFont()); 474 475 if (value instanceof Icon) { 476 setIcon((Icon)value); 477 setText(""); 478 } else { 479 String text = (value == null) ? " " : value.toString(); 480 481 if ("".equals(text)) { 482 text = " "; 483 } 484 setText(text); 485 } 486 487 // The renderer component should inherit the enabled and 488 // orientation state of its parent combobox. This is 489 // especially needed for GTK comboboxes, where the 490 // ListCellRenderer's state determines the visual state 491 // of the combobox. 492 if (comboBox != null){ 493 setEnabled(comboBox.isEnabled()); 494 setComponentOrientation(comboBox.getComponentOrientation()); 495 } 496 497 return this; 498 } 499 500 @Override 501 public void paint(Graphics g) { 502 super.paint(g); 503 SynthLookAndFeel.resetSelectedUI(); 504 } 505 } 506 507 508 private static class SynthComboBoxEditor 509 extends BasicComboBoxEditor.UIResource { 510 511 @Override public JTextField createEditorComponent() { 512 JTextField f = new JTextField("", 9); 513 f.setName("ComboBox.textField"); 514 return f; 515 } 516 } 517 518 519 /** 520 * Handles all the logic for treating the combo as a button when it is 521 * not editable, and when shouldActLikeButton() is true. This class is a 522 * special ButtonModel, and installed on the arrowButton when appropriate. 523 * It also is installed as a mouse listener and mouse motion listener on 524 * the combo box. In this way, the state between the button and combo 525 * are in sync. Whenever one is "over" both are. Whenever one is pressed, 526 * both are. 527 */ 528 private final class ButtonHandler extends DefaultButtonModel 529 implements MouseListener, PopupMenuListener { 530 /** 531 * Indicates that the mouse is over the combo or the arrow button. 532 * This field only has meaning if buttonWhenNotEnabled is true. 533 */ 534 private boolean over; 535 /** 536 * Indicates that the combo or arrow button has been pressed. This 537 * field only has meaning if buttonWhenNotEnabled is true. 538 */ 539 private boolean pressed; 540 541 //------------------------------------------------------------------ 542 // State Methods 543 //------------------------------------------------------------------ 544 545 /** 546 * <p>Updates the internal "pressed" state. If shouldActLikeButton() 547 * is true, and if this method call will change the internal state, 548 * then the combo and button will be repainted.</p> 549 * 550 * <p>Note that this method is called either when a press event 551 * occurs on the combo box, or on the arrow button.</p> 552 */ 553 private void updatePressed(boolean p) { 554 this.pressed = p && isEnabled(); 555 if (shouldActLikeButton()) { 556 comboBox.repaint(); 557 } 558 } 559 560 /** 561 * <p>Updates the internal "over" state. If shouldActLikeButton() 562 * is true, and if this method call will change the internal state, 563 * then the combo and button will be repainted.</p> 564 * 565 * <p>Note that this method is called either when a mouseover/mouseoff event 566 * occurs on the combo box, or on the arrow button.</p> 567 */ 568 private void updateOver(boolean o) { 569 boolean old = isRollover(); 570 this.over = o && isEnabled(); 571 boolean newo = isRollover(); 572 if (shouldActLikeButton() && old != newo) { 573 comboBox.repaint(); 574 } 575 } 576 577 //------------------------------------------------------------------ 578 // DefaultButtonModel Methods 579 //------------------------------------------------------------------ 580 581 /** 582 * @inheritDoc 583 * 584 * Ensures that isPressed() will return true if the combo is pressed, 585 * or the arrowButton is pressed, <em>or</em> if the combo popup is 586 * visible. This is the case because a combo box looks pressed when 587 * the popup is visible, and so should the arrow button. 588 */ 589 @Override 590 public boolean isPressed() { 591 boolean b = shouldActLikeButton() ? pressed : super.isPressed(); 592 return b || (pressedWhenPopupVisible && comboBox.isPopupVisible()); 593 } 594 595 /** 596 * @inheritDoc 597 * 598 * Ensures that the armed state is in sync with the pressed state 599 * if shouldActLikeButton is true. Without this method, the arrow 600 * button will not look pressed when the popup is open, regardless 601 * of the result of isPressed() alone. 602 */ 603 @Override 604 public boolean isArmed() { 605 boolean b = shouldActLikeButton() || 606 (pressedWhenPopupVisible && comboBox.isPopupVisible()); 607 return b ? isPressed() : super.isArmed(); 608 } 609 610 /** 611 * @inheritDoc 612 * 613 * Ensures that isRollover() will return true if the combo is 614 * rolled over, or the arrowButton is rolled over. 615 */ 616 @Override 617 public boolean isRollover() { 618 return shouldActLikeButton() ? over : super.isRollover(); 619 } 620 621 /** 622 * @inheritDoc 623 * 624 * Forwards pressed states to the internal "pressed" field 625 */ 626 @Override 627 public void setPressed(boolean b) { 628 super.setPressed(b); 629 updatePressed(b); 630 } 631 632 /** 633 * @inheritDoc 634 * 635 * Forwards rollover states to the internal "over" field 636 */ 637 @Override 638 public void setRollover(boolean b) { 639 super.setRollover(b); 640 updateOver(b); 641 } 642 643 //------------------------------------------------------------------ 644 // MouseListener/MouseMotionListener Methods 645 //------------------------------------------------------------------ 646 647 @Override 648 public void mouseEntered(MouseEvent mouseEvent) { 649 updateOver(true); 650 } 651 652 @Override 653 public void mouseExited(MouseEvent mouseEvent) { 654 updateOver(false); 655 } 656 657 @Override 658 public void mousePressed(MouseEvent mouseEvent) { 659 updatePressed(true); 660 } 661 662 @Override 663 public void mouseReleased(MouseEvent mouseEvent) { 664 updatePressed(false); 665 } 666 667 @Override 668 public void mouseClicked(MouseEvent e) {} 669 670 //------------------------------------------------------------------ 671 // PopupMenuListener Methods 672 //------------------------------------------------------------------ 673 674 /** 675 * @inheritDoc 676 * 677 * Ensures that the combo box is repainted when the popup is closed. 678 * This avoids a bug where clicking off the combo wasn't causing a repaint, 679 * and thus the combo box still looked pressed even when it was not. 680 * 681 * This bug was only noticed when acting as a button, but may be generally 682 * present. If so, remove the if() block 683 */ 684 @Override 685 public void popupMenuCanceled(PopupMenuEvent e) { 686 if (shouldActLikeButton() || pressedWhenPopupVisible) { 687 comboBox.repaint(); 688 } 689 } 690 691 @Override 692 public void popupMenuWillBecomeVisible(PopupMenuEvent e) {} 693 @Override 694 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {} 695 } 696 697 /** 698 * Handler for repainting combo when editor component gains/looses focus 699 */ 700 private static class EditorFocusHandler implements FocusListener, 701 PropertyChangeListener { 702 private JComboBox comboBox; 703 private ComboBoxEditor editor = null; 704 private Component editorComponent = null; 705 706 private EditorFocusHandler(JComboBox comboBox) { 707 this.comboBox = comboBox; 708 editor = comboBox.getEditor(); 709 if (editor != null){ 710 editorComponent = editor.getEditorComponent(); 711 if (editorComponent != null){ 712 editorComponent.addFocusListener(this); 713 } 714 } 715 comboBox.addPropertyChangeListener("editor",this); 716 } 717 718 public void unregister(){ 719 comboBox.removePropertyChangeListener(this); 720 if (editorComponent!=null){ 721 editorComponent.removeFocusListener(this); 722 } 723 } 724 725 /** Invoked when a component gains the keyboard focus. */ 726 public void focusGained(FocusEvent e) { 727 // repaint whole combo on focus gain 728 comboBox.repaint(); 729 } 730 731 /** Invoked when a component loses the keyboard focus. */ 732 public void focusLost(FocusEvent e) { 733 // repaint whole combo on focus loss 734 comboBox.repaint(); 735 } 736 737 /** 738 * Called when the combos editor changes 739 * 740 * @param evt A PropertyChangeEvent object describing the event source and 741 * the property that has changed. 742 */ 743 public void propertyChange(PropertyChangeEvent evt) { 744 ComboBoxEditor newEditor = comboBox.getEditor(); 745 if (editor != newEditor){ 746 if (editorComponent!=null){ 747 editorComponent.removeFocusListener(this); 748 } 749 editor = newEditor; 750 if (editor != null){ 751 editorComponent = editor.getEditorComponent(); 752 if (editorComponent != null){ 753 editorComponent.addFocusListener(this); 754 } 755 } 756 } 757 } 758 } 759 }