1 /* 2 * Copyright (c) 2011, 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 com.apple.laf; 27 28 import java.awt.*; 29 import java.awt.event.*; 30 31 import javax.accessibility.*; 32 import javax.swing.*; 33 import javax.swing.border.Border; 34 import javax.swing.event.*; 35 import javax.swing.plaf.*; 36 import javax.swing.plaf.basic.*; 37 import com.apple.laf.ClientPropertyApplicator.Property; 38 import apple.laf.JRSUIConstants.Size; 39 40 import com.apple.laf.AquaUtilControlSize.Sizeable; 41 import com.apple.laf.AquaUtils.RecyclableSingleton; 42 43 // Inspired by MetalComboBoxUI, which also has a combined text-and-arrow button for noneditables 44 public class AquaComboBoxUI extends BasicComboBoxUI implements Sizeable { 45 static final String POPDOWN_CLIENT_PROPERTY_KEY = "JComboBox.isPopDown"; 46 static final String ISSQUARE_CLIENT_PROPERTY_KEY = "JComboBox.isSquare"; 47 48 public static ComponentUI createUI(final JComponent c) { 49 return new AquaComboBoxUI(); 50 } 51 52 private boolean wasOpaque; 53 public void installUI(final JComponent c) { 54 super.installUI(c); 55 56 // this doesn't work right now, because the JComboBox.init() method calls 57 // .setOpaque(false) directly, and doesn't allow the LaF to decided. Bad Sun! 58 LookAndFeel.installProperty(c, "opaque", Boolean.FALSE); 59 60 wasOpaque = c.isOpaque(); 61 c.setOpaque(false); 62 } 63 64 public void uninstallUI(final JComponent c) { 65 c.setOpaque(wasOpaque); 66 super.uninstallUI(c); 67 } 68 69 protected void installListeners() { 70 super.installListeners(); 71 AquaUtilControlSize.addSizePropertyListener(comboBox); 72 } 73 74 protected void uninstallListeners() { 75 AquaUtilControlSize.removeSizePropertyListener(comboBox); 76 super.uninstallListeners(); 77 } 78 79 protected void installComponents() { 80 super.installComponents(); 81 82 // client properties must be applied after the components have been installed, 83 // because isSquare and isPopdown are applied to the installed button 84 getApplicator().attachAndApplyClientProperties(comboBox); 85 } 86 87 protected void uninstallComponents() { 88 getApplicator().removeFrom(comboBox); 89 super.uninstallComponents(); 90 } 91 92 protected ItemListener createItemListener() { 93 return new ItemListener() { 94 long lastBlink = 0L; 95 public void itemStateChanged(final ItemEvent e) { 96 if (e.getStateChange() != ItemEvent.SELECTED) return; 97 if (!popup.isVisible()) return; 98 99 // sometimes, multiple selection changes can occur while the popup is up, 100 // and blinking more than "once" (in a second) is not desirable 101 final long now = System.currentTimeMillis(); 102 if (now - 1000 < lastBlink) return; 103 lastBlink = now; 104 105 final JList itemList = popup.getList(); 106 final ListUI listUI = itemList.getUI(); 107 if (!(listUI instanceof AquaListUI)) return; 108 final AquaListUI aquaListUI = (AquaListUI)listUI; 109 110 final int selectedIndex = comboBox.getSelectedIndex(); 111 final ListModel dataModel = itemList.getModel(); 112 if (dataModel == null) return; 113 114 final Object value = dataModel.getElementAt(selectedIndex); 115 AquaUtils.blinkMenu(new AquaUtils.Selectable() { 116 public void paintSelected(final boolean selected) { 117 aquaListUI.repaintCell(value, selectedIndex, selected); 118 } 119 }); 120 } 121 }; 122 } 123 124 public void paint(final Graphics g, final JComponent c) { 125 // this space intentionally left blank 126 } 127 128 protected ListCellRenderer createRenderer() { 129 return new AquaComboBoxRenderer(comboBox); 130 } 131 132 protected ComboPopup createPopup() { 133 return new AquaComboBoxPopup(comboBox); 134 } 135 136 protected JButton createArrowButton() { 137 return new AquaComboBoxButton(this, comboBox, currentValuePane, listBox); 138 } 139 140 protected ComboBoxEditor createEditor() { 141 return new AquaComboBoxEditor(); 142 } 143 144 final class AquaComboBoxEditor extends BasicComboBoxEditor 145 implements UIResource, DocumentListener { 146 147 AquaComboBoxEditor() { 148 super(); 149 editor = new AquaCustomComboTextField(); 150 editor.addFocusListener(this); 151 editor.getDocument().addDocumentListener(this); 152 } 153 154 @Override 155 public void focusGained(final FocusEvent e) { 156 if (arrowButton != null) { 157 arrowButton.repaint(); 158 } 159 } 160 161 @Override 162 public void focusLost(final FocusEvent e) { 163 if (arrowButton != null) { 164 arrowButton.repaint(); 165 } 166 } 167 168 @Override 169 public void changedUpdate(final DocumentEvent e) { 170 editorTextChanged(); 171 } 172 173 @Override 174 public void insertUpdate(final DocumentEvent e) { 175 editorTextChanged(); 176 } 177 178 @Override 179 public void removeUpdate(final DocumentEvent e) { 180 editorTextChanged(); 181 } 182 183 private void editorTextChanged() { 184 if (!popup.isVisible()) return; 185 186 final Object text = editor.getText(); 187 188 final ListModel model = listBox.getModel(); 189 final int items = model.getSize(); 190 for (int i = 0; i < items; i++) { 191 final Object element = model.getElementAt(i); 192 if (element == null) continue; 193 194 final String asString = element.toString(); 195 if (asString == null || !asString.equals(text)) continue; 196 197 popup.getList().setSelectedIndex(i); 198 return; 199 } 200 201 popup.getList().clearSelection(); 202 } 203 } 204 205 class AquaCustomComboTextField extends JTextField { 206 public AquaCustomComboTextField() { 207 final InputMap inputMap = getInputMap(); 208 inputMap.put(KeyStroke.getKeyStroke("DOWN"), highlightNextAction); 209 inputMap.put(KeyStroke.getKeyStroke("KP_DOWN"), highlightNextAction); 210 inputMap.put(KeyStroke.getKeyStroke("UP"), highlightPreviousAction); 211 inputMap.put(KeyStroke.getKeyStroke("KP_UP"), highlightPreviousAction); 212 213 inputMap.put(KeyStroke.getKeyStroke("HOME"), highlightFirstAction); 214 inputMap.put(KeyStroke.getKeyStroke("END"), highlightLastAction); 215 inputMap.put(KeyStroke.getKeyStroke("PAGE_UP"), highlightPageUpAction); 216 inputMap.put(KeyStroke.getKeyStroke("PAGE_DOWN"), highlightPageDownAction); 217 218 final Action action = getActionMap().get(JTextField.notifyAction); 219 inputMap.put(KeyStroke.getKeyStroke("ENTER"), new AbstractAction() { 220 public void actionPerformed(final ActionEvent e) { 221 if (popup.isVisible()) { 222 triggerSelectionEvent(comboBox, e); 223 224 if (editor instanceof AquaCustomComboTextField) { 225 ((AquaCustomComboTextField)editor).selectAll(); 226 } 227 } else { 228 action.actionPerformed(e); 229 } 230 } 231 }); 232 } 233 234 // workaround for 4530952 235 public void setText(final String s) { 236 if (getText().equals(s)) { 237 return; 238 } 239 super.setText(s); 240 } 241 } 242 243 /** 244 * This listener hides the popup when the focus is lost. It also repaints 245 * when focus is gained or lost. 246 * 247 * This override is necessary because the Basic L&F for the combo box is working 248 * around a Solaris-only bug that we don't have on Mac OS X. So, remove the lightweight 249 * popup check here. rdar://Problem/3518582 250 */ 251 protected FocusListener createFocusListener() { 252 return new BasicComboBoxUI.FocusHandler() { 253 public void focusLost(final FocusEvent e) { 254 hasFocus = false; 255 if (!e.isTemporary()) { 256 setPopupVisible(comboBox, false); 257 } 258 comboBox.repaint(); 259 260 // Notify assistive technologies that the combo box lost focus 261 final AccessibleContext ac = ((Accessible)comboBox).getAccessibleContext(); 262 if (ac != null) { 263 ac.firePropertyChange(AccessibleContext.ACCESSIBLE_STATE_PROPERTY, AccessibleState.FOCUSED, null); 264 } 265 } 266 }; 267 } 268 269 protected void installKeyboardActions() { 270 super.installKeyboardActions(); 271 272 ActionMap actionMap = new ActionMapUIResource(); 273 274 actionMap.put("aquaSelectNext", highlightNextAction); 275 actionMap.put("aquaSelectPrevious", highlightPreviousAction); 276 actionMap.put("aquaEnterPressed", triggerSelectionAction); 277 actionMap.put("aquaSpacePressed", toggleSelectionAction); 278 279 actionMap.put("aquaSelectHome", highlightFirstAction); 280 actionMap.put("aquaSelectEnd", highlightLastAction); 281 actionMap.put("aquaSelectPageUp", highlightPageUpAction); 282 actionMap.put("aquaSelectPageDown", highlightPageDownAction); 283 284 actionMap.put("aquaHidePopup", hideAction); 285 286 SwingUtilities.replaceUIActionMap(comboBox, actionMap); 287 } 288 289 private abstract class ComboBoxAction extends AbstractAction { 290 public void actionPerformed(final ActionEvent e) { 291 if (!comboBox.isEnabled() || !comboBox.isShowing()) { 292 return; 293 } 294 295 if (comboBox.isPopupVisible()) { 296 final AquaComboBoxUI ui = (AquaComboBoxUI)comboBox.getUI(); 297 performComboBoxAction(ui); 298 } else { 299 comboBox.setPopupVisible(true); 300 } 301 } 302 303 abstract void performComboBoxAction(final AquaComboBoxUI ui); 304 } 305 306 /** 307 * Hilight _but do not select_ the next item in the list. 308 */ 309 private Action highlightNextAction = new ComboBoxAction() { 310 @Override 311 public void performComboBoxAction(AquaComboBoxUI ui) { 312 final int si = listBox.getSelectedIndex(); 313 314 if (si < comboBox.getModel().getSize() - 1) { 315 listBox.setSelectedIndex(si + 1); 316 listBox.ensureIndexIsVisible(si + 1); 317 } 318 comboBox.repaint(); 319 } 320 }; 321 322 /** 323 * Hilight _but do not select_ the previous item in the list. 324 */ 325 private Action highlightPreviousAction = new ComboBoxAction() { 326 @Override 327 void performComboBoxAction(final AquaComboBoxUI ui) { 328 final int si = listBox.getSelectedIndex(); 329 if (si > 0) { 330 listBox.setSelectedIndex(si - 1); 331 listBox.ensureIndexIsVisible(si - 1); 332 } 333 comboBox.repaint(); 334 } 335 }; 336 337 private Action highlightFirstAction = new ComboBoxAction() { 338 @Override 339 void performComboBoxAction(final AquaComboBoxUI ui) { 340 listBox.setSelectedIndex(0); 341 listBox.ensureIndexIsVisible(0); 342 } 343 }; 344 345 private Action highlightLastAction = new ComboBoxAction() { 346 @Override 347 void performComboBoxAction(final AquaComboBoxUI ui) { 348 final int size = listBox.getModel().getSize(); 349 listBox.setSelectedIndex(size - 1); 350 listBox.ensureIndexIsVisible(size - 1); 351 } 352 }; 353 354 private Action highlightPageUpAction = new ComboBoxAction() { 355 @Override 356 void performComboBoxAction(final AquaComboBoxUI ui) { 357 final int current = listBox.getSelectedIndex(); 358 final int first = listBox.getFirstVisibleIndex(); 359 360 if (current != first) { 361 listBox.setSelectedIndex(first); 362 return; 363 } 364 365 final int page = listBox.getVisibleRect().height / listBox.getCellBounds(0, 0).height; 366 int target = first - page; 367 if (target < 0) target = 0; 368 369 listBox.ensureIndexIsVisible(target); 370 listBox.setSelectedIndex(target); 371 } 372 }; 373 374 private Action highlightPageDownAction = new ComboBoxAction() { 375 @Override 376 void performComboBoxAction(final AquaComboBoxUI ui) { 377 final int current = listBox.getSelectedIndex(); 378 final int last = listBox.getLastVisibleIndex(); 379 380 if (current != last) { 381 listBox.setSelectedIndex(last); 382 return; 383 } 384 385 final int page = listBox.getVisibleRect().height / listBox.getCellBounds(0, 0).height; 386 final int end = listBox.getModel().getSize() - 1; 387 int target = last + page; 388 if (target > end) target = end; 389 390 listBox.ensureIndexIsVisible(target); 391 listBox.setSelectedIndex(target); 392 } 393 }; 394 395 // For <rdar://problem/3759984> Java 1.4.2_5: Serializing Swing components not working 396 // Inner classes were using a this reference and then trying to serialize the AquaComboBoxUI 397 // We shouldn't do that. But we need to be able to get the popup from other classes, so we need 398 // a public accessor. 399 public ComboPopup getPopup() { 400 return popup; 401 } 402 403 protected LayoutManager createLayoutManager() { 404 return new AquaComboBoxLayoutManager(); 405 } 406 407 class AquaComboBoxLayoutManager extends BasicComboBoxUI.ComboBoxLayoutManager { 408 public void layoutContainer(final Container parent) { 409 if (arrowButton != null && !comboBox.isEditable()) { 410 final Insets insets = comboBox.getInsets(); 411 final int width = comboBox.getWidth(); 412 final int height = comboBox.getHeight(); 413 arrowButton.setBounds(insets.left, insets.top, width - (insets.left + insets.right), height - (insets.top + insets.bottom)); 414 return; 415 } 416 417 final JComboBox cb = (JComboBox)parent; 418 final int width = cb.getWidth(); 419 final int height = cb.getHeight(); 420 421 final Insets insets = getInsets(); 422 final int buttonHeight = height - (insets.top + insets.bottom); 423 final int buttonWidth = 20; 424 425 if (arrowButton != null) { 426 arrowButton.setBounds(width - (insets.right + buttonWidth), insets.top, buttonWidth, buttonHeight); 427 } 428 429 if (editor != null) { 430 final Rectangle editorRect = rectangleForCurrentValue(); 431 editorRect.width += 4; 432 editor.setBounds(editorRect); 433 } 434 } 435 } 436 437 // This is here because Sun can't use protected like they should! 438 protected static final String IS_TABLE_CELL_EDITOR = "JComboBox.isTableCellEditor"; 439 440 protected static boolean isTableCellEditor(final JComponent c) { 441 return Boolean.TRUE.equals(c.getClientProperty(AquaComboBoxUI.IS_TABLE_CELL_EDITOR)); 442 } 443 444 protected static boolean isPopdown(final JComboBox c) { 445 return c.isEditable() || Boolean.TRUE.equals(c.getClientProperty(AquaComboBoxUI.POPDOWN_CLIENT_PROPERTY_KEY)); 446 } 447 448 protected static void triggerSelectionEvent(final JComboBox comboBox, final ActionEvent e) { 449 if (!comboBox.isEnabled()) return; 450 451 final AquaComboBoxUI aquaUi = (AquaComboBoxUI)comboBox.getUI(); 452 453 if (aquaUi.getPopup().getList().getSelectedIndex() < 0) { 454 comboBox.setPopupVisible(false); 455 } 456 457 if (isTableCellEditor(comboBox)) { 458 // Forces the selection of the list item if the combo box is in a JTable 459 comboBox.setSelectedIndex(aquaUi.getPopup().getList().getSelectedIndex()); 460 return; 461 } 462 463 if (comboBox.isPopupVisible()) { 464 comboBox.setSelectedIndex(aquaUi.getPopup().getList().getSelectedIndex()); 465 comboBox.setPopupVisible(false); 466 return; 467 } 468 469 // Call the default button binding. 470 // This is a pretty messy way of passing an event through to the root pane 471 final JRootPane root = SwingUtilities.getRootPane(comboBox); 472 if (root == null) return; 473 474 final InputMap im = root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); 475 final ActionMap am = root.getActionMap(); 476 if (im == null || am == null) return; 477 478 final Object obj = im.get(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0)); 479 if (obj == null) return; 480 481 final Action action = am.get(obj); 482 if (action == null) return; 483 484 action.actionPerformed(new ActionEvent(root, e.getID(), e.getActionCommand(), e.getWhen(), e.getModifiers())); 485 } 486 487 // This is somewhat messy. The difference here from BasicComboBoxUI.EnterAction is that 488 // arrow up or down does not automatically select the 489 private static final Action triggerSelectionAction = new AbstractAction() { 490 public void actionPerformed(final ActionEvent e) { 491 triggerSelectionEvent((JComboBox)e.getSource(), e); 492 } 493 }; 494 495 private static final Action toggleSelectionAction = new AbstractAction() { 496 public void actionPerformed(final ActionEvent e) { 497 final JComboBox comboBox = (JComboBox)e.getSource(); 498 if (!comboBox.isEnabled()) return; 499 if (comboBox.isEditable()) return; 500 501 final AquaComboBoxUI aquaUi = (AquaComboBoxUI)comboBox.getUI(); 502 503 if (comboBox.isPopupVisible()) { 504 comboBox.setSelectedIndex(aquaUi.getPopup().getList().getSelectedIndex()); 505 comboBox.setPopupVisible(false); 506 return; 507 } 508 509 comboBox.setPopupVisible(true); 510 } 511 }; 512 513 private static Action hideAction = new AbstractAction() { 514 @Override 515 public void actionPerformed(final ActionEvent e) { 516 final JComboBox comboBox = (JComboBox)e.getSource(); 517 518 if (comboBox.isPopupVisible()) { 519 comboBox.firePopupMenuCanceled(); 520 comboBox.setPopupVisible(false); 521 } 522 } 523 }; 524 525 public void applySizeFor(final JComponent c, final Size size) { 526 if (arrowButton == null) return; 527 final Border border = arrowButton.getBorder(); 528 if (!(border instanceof AquaButtonBorder)) return; 529 final AquaButtonBorder aquaBorder = (AquaButtonBorder)border; 530 arrowButton.setBorder(aquaBorder.deriveBorderForSize(size)); 531 } 532 533 public Dimension getMinimumSize(final JComponent c) { 534 if (!isMinimumSizeDirty) { 535 return new Dimension(cachedMinimumSize); 536 } 537 538 final boolean editable = comboBox.isEditable(); 539 540 final Dimension size; 541 if (!editable && arrowButton != null && arrowButton instanceof AquaComboBoxButton) { 542 final AquaComboBoxButton button = (AquaComboBoxButton)arrowButton; 543 final Insets buttonInsets = button.getInsets(); 544 // Insets insets = comboBox.getInsets(); 545 final Insets insets = new Insets(0, 5, 0, 25);//comboBox.getInsets(); 546 547 size = getDisplaySize(); 548 size.width += insets.left + insets.right; 549 size.width += buttonInsets.left + buttonInsets.right; 550 size.width += buttonInsets.right + 10; 551 size.height += insets.top + insets.bottom; 552 size.height += buttonInsets.top + buttonInsets.bottom; 553 // Min height = Height of arrow button plus 2 pixels fuzz above plus 2 below. 23 + 2 + 2 554 size.height = Math.max(27, size.height); 555 } else if (editable && arrowButton != null && editor != null) { 556 size = super.getMinimumSize(c); 557 final Insets margin = arrowButton.getMargin(); 558 size.height += margin.top + margin.bottom; 559 } else { 560 size = super.getMinimumSize(c); 561 } 562 563 final Border border = c.getBorder(); 564 if (border != null) { 565 final Insets insets = border.getBorderInsets(c); 566 size.height += insets.top + insets.bottom; 567 size.width += insets.left + insets.right; 568 } 569 570 cachedMinimumSize.setSize(size.width, size.height); 571 isMinimumSizeDirty = false; 572 573 return new Dimension(cachedMinimumSize); 574 } 575 576 @SuppressWarnings("unchecked") 577 static final RecyclableSingleton<ClientPropertyApplicator<JComboBox, AquaComboBoxUI>> APPLICATOR = new RecyclableSingleton<ClientPropertyApplicator<JComboBox, AquaComboBoxUI>>() { 578 @Override 579 protected ClientPropertyApplicator<JComboBox, AquaComboBoxUI> getInstance() { 580 return new ClientPropertyApplicator<JComboBox, AquaComboBoxUI>( 581 new Property<AquaComboBoxUI>(AquaFocusHandler.FRAME_ACTIVE_PROPERTY) { 582 public void applyProperty(final AquaComboBoxUI target, final Object value) { 583 if (Boolean.FALSE.equals(value)) { 584 if (target.comboBox != null) target.comboBox.hidePopup(); 585 } 586 if (target.listBox != null) target.listBox.repaint(); 587 } 588 }, 589 new Property<AquaComboBoxUI>("editable") { 590 public void applyProperty(final AquaComboBoxUI target, final Object value) { 591 if (target.comboBox == null) return; 592 target.comboBox.repaint(); 593 } 594 }, 595 new Property<AquaComboBoxUI>("background") { 596 public void applyProperty(final AquaComboBoxUI target, final Object value) { 597 final Color color = (Color)value; 598 if (target.arrowButton != null) target.arrowButton.setBackground(color); 599 if (target.listBox != null) target.listBox.setBackground(color); 600 } 601 }, 602 new Property<AquaComboBoxUI>("foreground") { 603 public void applyProperty(final AquaComboBoxUI target, final Object value) { 604 final Color color = (Color)value; 605 if (target.arrowButton != null) target.arrowButton.setForeground(color); 606 if (target.listBox != null) target.listBox.setForeground(color); 607 } 608 }, 609 new Property<AquaComboBoxUI>(POPDOWN_CLIENT_PROPERTY_KEY) { 610 public void applyProperty(final AquaComboBoxUI target, final Object value) { 611 if (!(target.arrowButton instanceof AquaComboBoxButton)) return; 612 ((AquaComboBoxButton)target.arrowButton).setIsPopDown(Boolean.TRUE.equals(value)); 613 } 614 }, 615 new Property<AquaComboBoxUI>(ISSQUARE_CLIENT_PROPERTY_KEY) { 616 public void applyProperty(final AquaComboBoxUI target, final Object value) { 617 if (!(target.arrowButton instanceof AquaComboBoxButton)) return; 618 ((AquaComboBoxButton)target.arrowButton).setIsSquare(Boolean.TRUE.equals(value)); 619 } 620 } 621 ) { 622 public AquaComboBoxUI convertJComponentToTarget(final JComboBox combo) { 623 final ComboBoxUI comboUI = combo.getUI(); 624 if (comboUI instanceof AquaComboBoxUI) return (AquaComboBoxUI)comboUI; 625 return null; 626 } 627 }; 628 } 629 }; 630 static ClientPropertyApplicator<JComboBox, AquaComboBoxUI> getApplicator() { 631 return APPLICATOR.get(); 632 } 633 }