1 /* 2 * Copyright (c) 2002, 2013, 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<Object>, UIResource { 448 public SynthComboBoxRenderer() { 449 super(); 450 setText(" "); 451 } 452 453 @Override 454 public String getName() { 455 // SynthComboBoxRenderer should have installed Name while constructor is working. 456 // The setName invocation in the SynthComboBoxRenderer() constructor doesn't work 457 // because of the opaque property is installed in the constructor based on the 458 // component name (see GTKStyle.isOpaque()) 459 String name = super.getName(); 460 461 return name == null ? "ComboBox.renderer" : name; 462 } 463 464 @Override 465 public Component getListCellRendererComponent(JList<?> list, Object value, 466 int index, boolean isSelected, boolean cellHasFocus) { 467 setName("ComboBox.listRenderer"); 468 SynthLookAndFeel.resetSelectedUI(); 469 if (isSelected) { 470 setBackground(list.getSelectionBackground()); 471 setForeground(list.getSelectionForeground()); 472 if (!useListColors) { 473 SynthLookAndFeel.setSelectedUI( 474 (SynthLabelUI)SynthLookAndFeel.getUIOfType(getUI(), 475 SynthLabelUI.class), isSelected, cellHasFocus, 476 list.isEnabled(), false); 477 } 478 } else { 479 setBackground(list.getBackground()); 480 setForeground(list.getForeground()); 481 } 482 483 setFont(list.getFont()); 484 485 if (value instanceof Icon) { 486 setIcon((Icon)value); 487 setText(""); 488 } else { 489 String text = (value == null) ? " " : value.toString(); 490 491 if ("".equals(text)) { 492 text = " "; 493 } 494 setText(text); 495 } 496 497 // The renderer component should inherit the enabled and 498 // orientation state of its parent combobox. This is 499 // especially needed for GTK comboboxes, where the 500 // ListCellRenderer's state determines the visual state 501 // of the combobox. 502 if (comboBox != null){ 503 setEnabled(comboBox.isEnabled()); 504 setComponentOrientation(comboBox.getComponentOrientation()); 505 } 506 507 return this; 508 } 509 510 @Override 511 public void paint(Graphics g) { 512 super.paint(g); 513 SynthLookAndFeel.resetSelectedUI(); 514 } 515 } 516 517 518 private static class SynthComboBoxEditor 519 extends BasicComboBoxEditor.UIResource { 520 521 @Override public JTextField createEditorComponent() { 522 JTextField f = new JTextField("", 9); 523 f.setName("ComboBox.textField"); 524 return f; 525 } 526 } 527 528 529 /** 530 * Handles all the logic for treating the combo as a button when it is 531 * not editable, and when shouldActLikeButton() is true. This class is a 532 * special ButtonModel, and installed on the arrowButton when appropriate. 533 * It also is installed as a mouse listener and mouse motion listener on 534 * the combo box. In this way, the state between the button and combo 535 * are in sync. Whenever one is "over" both are. Whenever one is pressed, 536 * both are. 537 */ 538 private final class ButtonHandler extends DefaultButtonModel 539 implements MouseListener, PopupMenuListener { 540 /** 541 * Indicates that the mouse is over the combo or the arrow button. 542 * This field only has meaning if buttonWhenNotEnabled is true. 543 */ 544 private boolean over; 545 /** 546 * Indicates that the combo or arrow button has been pressed. This 547 * field only has meaning if buttonWhenNotEnabled is true. 548 */ 549 private boolean pressed; 550 551 //------------------------------------------------------------------ 552 // State Methods 553 //------------------------------------------------------------------ 554 555 /** 556 * <p>Updates the internal "pressed" state. If shouldActLikeButton() 557 * is true, and if this method call will change the internal state, 558 * then the combo and button will be repainted.</p> 559 * 560 * <p>Note that this method is called either when a press event 561 * occurs on the combo box, or on the arrow button.</p> 562 */ 563 private void updatePressed(boolean p) { 564 this.pressed = p && isEnabled(); 565 if (shouldActLikeButton()) { 566 comboBox.repaint(); 567 } 568 } 569 570 /** 571 * <p>Updates the internal "over" state. If shouldActLikeButton() 572 * is true, and if this method call will change the internal state, 573 * then the combo and button will be repainted.</p> 574 * 575 * <p>Note that this method is called either when a mouseover/mouseoff event 576 * occurs on the combo box, or on the arrow button.</p> 577 */ 578 private void updateOver(boolean o) { 579 boolean old = isRollover(); 580 this.over = o && isEnabled(); 581 boolean newo = isRollover(); 582 if (shouldActLikeButton() && old != newo) { 583 comboBox.repaint(); 584 } 585 } 586 587 //------------------------------------------------------------------ 588 // DefaultButtonModel Methods 589 //------------------------------------------------------------------ 590 591 /** 592 * @inheritDoc 593 * 594 * Ensures that isPressed() will return true if the combo is pressed, 595 * or the arrowButton is pressed, <em>or</em> if the combo popup is 596 * visible. This is the case because a combo box looks pressed when 597 * the popup is visible, and so should the arrow button. 598 */ 599 @Override 600 public boolean isPressed() { 601 boolean b = shouldActLikeButton() ? pressed : super.isPressed(); 602 return b || (pressedWhenPopupVisible && comboBox.isPopupVisible()); 603 } 604 605 /** 606 * @inheritDoc 607 * 608 * Ensures that the armed state is in sync with the pressed state 609 * if shouldActLikeButton is true. Without this method, the arrow 610 * button will not look pressed when the popup is open, regardless 611 * of the result of isPressed() alone. 612 */ 613 @Override 614 public boolean isArmed() { 615 boolean b = shouldActLikeButton() || 616 (pressedWhenPopupVisible && comboBox.isPopupVisible()); 617 return b ? isPressed() : super.isArmed(); 618 } 619 620 /** 621 * @inheritDoc 622 * 623 * Ensures that isRollover() will return true if the combo is 624 * rolled over, or the arrowButton is rolled over. 625 */ 626 @Override 627 public boolean isRollover() { 628 return shouldActLikeButton() ? over : super.isRollover(); 629 } 630 631 /** 632 * @inheritDoc 633 * 634 * Forwards pressed states to the internal "pressed" field 635 */ 636 @Override 637 public void setPressed(boolean b) { 638 super.setPressed(b); 639 updatePressed(b); 640 } 641 642 /** 643 * @inheritDoc 644 * 645 * Forwards rollover states to the internal "over" field 646 */ 647 @Override 648 public void setRollover(boolean b) { 649 super.setRollover(b); 650 updateOver(b); 651 } 652 653 //------------------------------------------------------------------ 654 // MouseListener/MouseMotionListener Methods 655 //------------------------------------------------------------------ 656 657 @Override 658 public void mouseEntered(MouseEvent mouseEvent) { 659 updateOver(true); 660 } 661 662 @Override 663 public void mouseExited(MouseEvent mouseEvent) { 664 updateOver(false); 665 } 666 667 @Override 668 public void mousePressed(MouseEvent mouseEvent) { 669 updatePressed(true); 670 } 671 672 @Override 673 public void mouseReleased(MouseEvent mouseEvent) { 674 updatePressed(false); 675 } 676 677 @Override 678 public void mouseClicked(MouseEvent e) {} 679 680 //------------------------------------------------------------------ 681 // PopupMenuListener Methods 682 //------------------------------------------------------------------ 683 684 /** 685 * @inheritDoc 686 * 687 * Ensures that the combo box is repainted when the popup is closed. 688 * This avoids a bug where clicking off the combo wasn't causing a repaint, 689 * and thus the combo box still looked pressed even when it was not. 690 * 691 * This bug was only noticed when acting as a button, but may be generally 692 * present. If so, remove the if() block 693 */ 694 @Override 695 public void popupMenuCanceled(PopupMenuEvent e) { 696 if (shouldActLikeButton() || pressedWhenPopupVisible) { 697 comboBox.repaint(); 698 } 699 } 700 701 @Override 702 public void popupMenuWillBecomeVisible(PopupMenuEvent e) {} 703 @Override 704 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {} 705 } 706 707 /** 708 * Handler for repainting combo when editor component gains/looses focus 709 */ 710 private static class EditorFocusHandler implements FocusListener, 711 PropertyChangeListener { 712 private JComboBox comboBox; 713 private ComboBoxEditor editor = null; 714 private Component editorComponent = null; 715 716 private EditorFocusHandler(JComboBox comboBox) { 717 this.comboBox = comboBox; 718 editor = comboBox.getEditor(); 719 if (editor != null){ 720 editorComponent = editor.getEditorComponent(); 721 if (editorComponent != null){ 722 editorComponent.addFocusListener(this); 723 } 724 } 725 comboBox.addPropertyChangeListener("editor",this); 726 } 727 728 public void unregister(){ 729 comboBox.removePropertyChangeListener(this); 730 if (editorComponent!=null){ 731 editorComponent.removeFocusListener(this); 732 } 733 } 734 735 /** Invoked when a component gains the keyboard focus. */ 736 public void focusGained(FocusEvent e) { 737 // repaint whole combo on focus gain 738 comboBox.repaint(); 739 } 740 741 /** Invoked when a component loses the keyboard focus. */ 742 public void focusLost(FocusEvent e) { 743 // repaint whole combo on focus loss 744 comboBox.repaint(); 745 } 746 747 /** 748 * Called when the combos editor changes 749 * 750 * @param evt A PropertyChangeEvent object describing the event source and 751 * the property that has changed. 752 */ 753 public void propertyChange(PropertyChangeEvent evt) { 754 ComboBoxEditor newEditor = comboBox.getEditor(); 755 if (editor != newEditor){ 756 if (editorComponent!=null){ 757 editorComponent.removeFocusListener(this); 758 } 759 editor = newEditor; 760 if (editor != null){ 761 editorComponent = editor.getEditorComponent(); 762 if (editorComponent != null){ 763 editorComponent.addFocusListener(this); 764 } 765 } 766 } 767 } 768 } 769 }