1 /* 2 * Copyright (c) 2002, 2014, 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(c, style, state); 212 } 213 214 private int getComponentState(JComponent c) { 215 // currently we have a broken situation where if a developer 216 // takes the border from a JComboBox and sets it on a JTextField 217 // then the codepath will eventually lead back to this method 218 // but pass in a JTextField instead of JComboBox! In case this 219 // happens, we just return the normal synth state for the component 220 // instead of doing anything special 221 if (!(c instanceof JComboBox)) return SynthLookAndFeel.getComponentState(c); 222 223 JComboBox<?> box = (JComboBox)c; 224 if (shouldActLikeButton()) { 225 int state = ENABLED; 226 if ((!c.isEnabled())) { 227 state = DISABLED; 228 } 229 if (buttonHandler.isPressed()) { 230 state |= PRESSED; 231 } 232 if (buttonHandler.isRollover()) { 233 state |= MOUSE_OVER; 234 } 235 if (box.isFocusOwner()) { 236 state |= FOCUSED; 237 } 238 return state; 239 } else { 240 // for editable combos the editor component has the focus not the 241 // combo box its self, so we should make the combo paint focused 242 // when its editor has focus 243 int basicState = SynthLookAndFeel.getComponentState(c); 244 if (box.isEditable() && 245 box.getEditor().getEditorComponent().isFocusOwner()) { 246 basicState |= FOCUSED; 247 } 248 return basicState; 249 } 250 } 251 252 /** 253 * {@inheritDoc} 254 */ 255 @Override 256 protected ComboPopup createPopup() { 257 SynthComboPopup p = new SynthComboPopup(comboBox); 258 p.addPopupMenuListener(buttonHandler); 259 return p; 260 } 261 262 /** 263 * {@inheritDoc} 264 */ 265 @Override 266 protected ListCellRenderer<Object> createRenderer() { 267 return new SynthComboBoxRenderer(); 268 } 269 270 /** 271 * {@inheritDoc} 272 */ 273 @Override 274 protected ComboBoxEditor createEditor() { 275 return new SynthComboBoxEditor(); 276 } 277 278 // 279 // end UI Initialization 280 //====================== 281 282 /** 283 * {@inheritDoc} 284 */ 285 @Override 286 public void propertyChange(PropertyChangeEvent e) { 287 if (SynthLookAndFeel.shouldUpdateStyle(e)) { 288 updateStyle(comboBox); 289 } 290 } 291 292 /** 293 * {@inheritDoc} 294 */ 295 @Override 296 protected JButton createArrowButton() { 297 SynthArrowButton button = new SynthArrowButton(SwingConstants.SOUTH); 298 button.setName("ComboBox.arrowButton"); 299 button.setModel(buttonHandler); 300 return button; 301 } 302 303 //================================= 304 // begin ComponentUI Implementation 305 306 /** 307 * Notifies this UI delegate to repaint the specified component. 308 * This method paints the component background, then calls 309 * the {@link #paint(SynthContext,Graphics)} method. 310 * 311 * <p>In general, this method does not need to be overridden by subclasses. 312 * All Look and Feel rendering code should reside in the {@code paint} method. 313 * 314 * @param g the {@code Graphics} object used for painting 315 * @param c the component being painted 316 * @see #paint(SynthContext,Graphics) 317 */ 318 @Override 319 public void update(Graphics g, JComponent c) { 320 SynthContext context = getContext(c); 321 322 SynthLookAndFeel.update(context, g); 323 context.getPainter().paintComboBoxBackground(context, g, 0, 0, 324 c.getWidth(), c.getHeight()); 325 paint(context, g); 326 context.dispose(); 327 } 328 329 /** 330 * Paints the specified component according to the Look and Feel. 331 * <p>This method is not used by Synth Look and Feel. 332 * Painting is handled by the {@link #paint(SynthContext,Graphics)} method. 333 * 334 * @param g the {@code Graphics} object used for painting 335 * @param c the component being painted 336 * @see #paint(SynthContext,Graphics) 337 */ 338 @Override 339 public void paint(Graphics g, JComponent c) { 340 SynthContext context = getContext(c); 341 342 paint(context, g); 343 context.dispose(); 344 } 345 346 /** 347 * Paints the specified component. 348 * 349 * @param context context for the component being painted 350 * @param g the {@code Graphics} object used for painting 351 * @see #update(Graphics,JComponent) 352 */ 353 protected void paint(SynthContext context, Graphics g) { 354 hasFocus = comboBox.hasFocus(); 355 if ( !comboBox.isEditable() ) { 356 Rectangle r = rectangleForCurrentValue(); 357 paintCurrentValue(g,r,hasFocus); 358 } 359 } 360 361 /** 362 * {@inheritDoc} 363 */ 364 @Override 365 public void paintBorder(SynthContext context, Graphics g, int x, 366 int y, int w, int h) { 367 context.getPainter().paintComboBoxBorder(context, g, x, y, w, h); 368 } 369 370 /** 371 * Paints the currently selected item. 372 */ 373 @Override 374 public void paintCurrentValue(Graphics g,Rectangle bounds,boolean hasFocus) { 375 ListCellRenderer<Object> renderer = comboBox.getRenderer(); 376 Component c; 377 378 c = renderer.getListCellRendererComponent( 379 listBox, comboBox.getSelectedItem(), -1, false, false ); 380 381 // Fix for 4238829: should lay out the JPanel. 382 boolean shouldValidate = false; 383 if (c instanceof JPanel) { 384 shouldValidate = true; 385 } 386 387 if (c instanceof UIResource) { 388 c.setName("ComboBox.renderer"); 389 } 390 391 boolean force = forceOpaque && c instanceof JComponent; 392 if (force) { 393 ((JComponent)c).setOpaque(false); 394 } 395 396 int x = bounds.x, y = bounds.y, w = bounds.width, h = bounds.height; 397 if (padding != null) { 398 x = bounds.x + padding.left; 399 y = bounds.y + padding.top; 400 w = bounds.width - (padding.left + padding.right); 401 h = bounds.height - (padding.top + padding.bottom); 402 } 403 404 currentValuePane.paintComponent(g, c, comboBox, x, y, w, h, shouldValidate); 405 406 if (force) { 407 ((JComponent)c).setOpaque(true); 408 } 409 } 410 411 /** 412 * @return true if this combo box should act as one big button. Typically 413 * only happens when buttonWhenNotEditable is true, and comboBox.isEditable 414 * is false. 415 */ 416 private boolean shouldActLikeButton() { 417 return buttonWhenNotEditable && !comboBox.isEditable(); 418 } 419 420 /** 421 * Returns the default size of an empty display area of the combo box using 422 * the current renderer and font. 423 * 424 * This method was overridden to use SynthComboBoxRenderer instead of 425 * DefaultListCellRenderer as the default renderer when calculating the 426 * size of the combo box. This is used in the case of the combo not having 427 * any data. 428 * 429 * @return the size of an empty display area 430 * @see #getDisplaySize 431 */ 432 @Override 433 protected Dimension getDefaultSize() { 434 SynthComboBoxRenderer r = new SynthComboBoxRenderer(); 435 Dimension d = getSizeForComponent(r.getListCellRendererComponent(listBox, " ", -1, false, false)); 436 return new Dimension(d.width, d.height); 437 } 438 439 /** 440 * From BasicComboBoxRenderer v 1.18. 441 * 442 * Be aware that SynthFileChooserUIImpl relies on the fact that the default 443 * renderer installed on a Synth combo box is a JLabel. If this is changed, 444 * then an assert will fail in SynthFileChooserUIImpl 445 */ 446 @SuppressWarnings("serial") // Superclass is not serializable across versions 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 @SuppressWarnings("serial") // Superclass is not serializable across versions 539 private final class ButtonHandler extends DefaultButtonModel 540 implements MouseListener, PopupMenuListener { 541 /** 542 * Indicates that the mouse is over the combo or the arrow button. 543 * This field only has meaning if buttonWhenNotEnabled is true. 544 */ 545 private boolean over; 546 /** 547 * Indicates that the combo or arrow button has been pressed. This 548 * field only has meaning if buttonWhenNotEnabled is true. 549 */ 550 private boolean pressed; 551 552 //------------------------------------------------------------------ 553 // State Methods 554 //------------------------------------------------------------------ 555 556 /** 557 * <p>Updates the internal "pressed" state. If shouldActLikeButton() 558 * is true, and if this method call will change the internal state, 559 * then the combo and button will be repainted.</p> 560 * 561 * <p>Note that this method is called either when a press event 562 * occurs on the combo box, or on the arrow button.</p> 563 */ 564 private void updatePressed(boolean p) { 565 this.pressed = p && isEnabled(); 566 if (shouldActLikeButton()) { 567 comboBox.repaint(); 568 } 569 } 570 571 /** 572 * <p>Updates the internal "over" state. If shouldActLikeButton() 573 * is true, and if this method call will change the internal state, 574 * then the combo and button will be repainted.</p> 575 * 576 * <p>Note that this method is called either when a mouseover/mouseoff event 577 * occurs on the combo box, or on the arrow button.</p> 578 */ 579 private void updateOver(boolean o) { 580 boolean old = isRollover(); 581 this.over = o && isEnabled(); 582 boolean newo = isRollover(); 583 if (shouldActLikeButton() && old != newo) { 584 comboBox.repaint(); 585 } 586 } 587 588 //------------------------------------------------------------------ 589 // DefaultButtonModel Methods 590 //------------------------------------------------------------------ 591 592 /** 593 * @inheritDoc 594 * 595 * Ensures that isPressed() will return true if the combo is pressed, 596 * or the arrowButton is pressed, <em>or</em> if the combo popup is 597 * visible. This is the case because a combo box looks pressed when 598 * the popup is visible, and so should the arrow button. 599 */ 600 @Override 601 public boolean isPressed() { 602 boolean b = shouldActLikeButton() ? pressed : super.isPressed(); 603 return b || (pressedWhenPopupVisible && comboBox.isPopupVisible()); 604 } 605 606 /** 607 * @inheritDoc 608 * 609 * Ensures that the armed state is in sync with the pressed state 610 * if shouldActLikeButton is true. Without this method, the arrow 611 * button will not look pressed when the popup is open, regardless 612 * of the result of isPressed() alone. 613 */ 614 @Override 615 public boolean isArmed() { 616 boolean b = shouldActLikeButton() || 617 (pressedWhenPopupVisible && comboBox.isPopupVisible()); 618 return b ? isPressed() : super.isArmed(); 619 } 620 621 /** 622 * @inheritDoc 623 * 624 * Ensures that isRollover() will return true if the combo is 625 * rolled over, or the arrowButton is rolled over. 626 */ 627 @Override 628 public boolean isRollover() { 629 return shouldActLikeButton() ? over : super.isRollover(); 630 } 631 632 /** 633 * @inheritDoc 634 * 635 * Forwards pressed states to the internal "pressed" field 636 */ 637 @Override 638 public void setPressed(boolean b) { 639 super.setPressed(b); 640 updatePressed(b); 641 } 642 643 /** 644 * @inheritDoc 645 * 646 * Forwards rollover states to the internal "over" field 647 */ 648 @Override 649 public void setRollover(boolean b) { 650 super.setRollover(b); 651 updateOver(b); 652 } 653 654 //------------------------------------------------------------------ 655 // MouseListener/MouseMotionListener Methods 656 //------------------------------------------------------------------ 657 658 @Override 659 public void mouseEntered(MouseEvent mouseEvent) { 660 updateOver(true); 661 } 662 663 @Override 664 public void mouseExited(MouseEvent mouseEvent) { 665 updateOver(false); 666 } 667 668 @Override 669 public void mousePressed(MouseEvent mouseEvent) { 670 updatePressed(true); 671 } 672 673 @Override 674 public void mouseReleased(MouseEvent mouseEvent) { 675 updatePressed(false); 676 } 677 678 @Override 679 public void mouseClicked(MouseEvent e) {} 680 681 //------------------------------------------------------------------ 682 // PopupMenuListener Methods 683 //------------------------------------------------------------------ 684 685 /** 686 * @inheritDoc 687 * 688 * Ensures that the combo box is repainted when the popup is closed. 689 * This avoids a bug where clicking off the combo wasn't causing a repaint, 690 * and thus the combo box still looked pressed even when it was not. 691 * 692 * This bug was only noticed when acting as a button, but may be generally 693 * present. If so, remove the if() block 694 */ 695 @Override 696 public void popupMenuCanceled(PopupMenuEvent e) { 697 if (shouldActLikeButton() || pressedWhenPopupVisible) { 698 comboBox.repaint(); 699 } 700 } 701 702 @Override 703 public void popupMenuWillBecomeVisible(PopupMenuEvent e) {} 704 @Override 705 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {} 706 } 707 708 /** 709 * Handler for repainting combo when editor component gains/looses focus 710 */ 711 private static class EditorFocusHandler implements FocusListener, 712 PropertyChangeListener { 713 private JComboBox<?> comboBox; 714 private ComboBoxEditor editor = null; 715 private Component editorComponent = null; 716 717 private EditorFocusHandler(JComboBox<?> comboBox) { 718 this.comboBox = comboBox; 719 editor = comboBox.getEditor(); 720 if (editor != null){ 721 editorComponent = editor.getEditorComponent(); 722 if (editorComponent != null){ 723 editorComponent.addFocusListener(this); 724 } 725 } 726 comboBox.addPropertyChangeListener("editor",this); 727 } 728 729 public void unregister(){ 730 comboBox.removePropertyChangeListener(this); 731 if (editorComponent!=null){ 732 editorComponent.removeFocusListener(this); 733 } 734 } 735 736 /** Invoked when a component gains the keyboard focus. */ 737 public void focusGained(FocusEvent e) { 738 // repaint whole combo on focus gain 739 comboBox.repaint(); 740 } 741 742 /** Invoked when a component loses the keyboard focus. */ 743 public void focusLost(FocusEvent e) { 744 // repaint whole combo on focus loss 745 comboBox.repaint(); 746 } 747 748 /** 749 * Called when the combos editor changes 750 * 751 * @param evt A PropertyChangeEvent object describing the event source and 752 * the property that has changed. 753 */ 754 public void propertyChange(PropertyChangeEvent evt) { 755 ComboBoxEditor newEditor = comboBox.getEditor(); 756 if (editor != newEditor){ 757 if (editorComponent!=null){ 758 editorComponent.removeFocusListener(this); 759 } 760 editor = newEditor; 761 if (editor != null){ 762 editorComponent = editor.getEditorComponent(); 763 if (editorComponent != null){ 764 editorComponent.addFocusListener(this); 765 } 766 } 767 } 768 } 769 } 770 }