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