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 abstract class ComboBoxAction extends AbstractAction { 290 public void actionPerformed(final ActionEvent e) { 291 if (!comboBox.isEnabled() || !comboBox.isShowing()) return; 292 293 if (comboBox.isPopupVisible()) { 294 final AquaComboBoxUI ui = (AquaComboBoxUI)comboBox.getUI(); 295 performComboBoxAction(ui); 296 } else { 297 comboBox.setPopupVisible(true); 298 } 299 } 300 301 abstract void performComboBoxAction(final AquaComboBoxUI ui); 302 } 303 304 /** 305 * Hilight _but do not select_ the next item in the list. 306 */ 307 Action highlightNextAction = new ComboBoxAction() { 308 @Override 309 public void performComboBoxAction(AquaComboBoxUI ui) { 310 final int si = listBox.getSelectedIndex(); 311 312 if (si < comboBox.getModel().getSize() - 1) { 313 listBox.setSelectedIndex(si + 1); 314 listBox.ensureIndexIsVisible(si + 1); 315 } 316 comboBox.repaint(); 317 } 318 }; 319 320 /** 321 * Hilight _but do not select_ the previous item in the list. 322 */ 323 Action highlightPreviousAction = new ComboBoxAction() { 324 @Override 325 void performComboBoxAction(final AquaComboBoxUI ui) { 326 final int si = listBox.getSelectedIndex(); 327 if (si > 0) { 328 listBox.setSelectedIndex(si - 1); 329 listBox.ensureIndexIsVisible(si - 1); 330 } 331 comboBox.repaint(); 332 } 333 }; 334 335 Action highlightFirstAction = new ComboBoxAction() { 336 @Override 337 void performComboBoxAction(final AquaComboBoxUI ui) { 338 listBox.setSelectedIndex(0); 339 listBox.ensureIndexIsVisible(0); 340 } 341 }; 342 343 Action highlightLastAction = new ComboBoxAction() { 344 @Override 345 void performComboBoxAction(final AquaComboBoxUI ui) { 346 final int size = listBox.getModel().getSize(); 347 listBox.setSelectedIndex(size - 1); 348 listBox.ensureIndexIsVisible(size - 1); 349 } 350 }; 351 352 Action highlightPageUpAction = new ComboBoxAction() { 353 @Override 354 void performComboBoxAction(final AquaComboBoxUI ui) { 355 final int current = listBox.getSelectedIndex(); 356 final int first = listBox.getFirstVisibleIndex(); 357 358 if (current != first) { 359 listBox.setSelectedIndex(first); 360 return; 361 } 362 363 final int page = listBox.getVisibleRect().height / listBox.getCellBounds(0, 0).height; 364 int target = first - page; 365 if (target < 0) target = 0; 366 367 listBox.ensureIndexIsVisible(target); 368 listBox.setSelectedIndex(target); 369 } 370 }; 371 372 Action highlightPageDownAction = new ComboBoxAction() { 373 @Override 374 void performComboBoxAction(final AquaComboBoxUI ui) { 375 final int current = listBox.getSelectedIndex(); 376 final int last = listBox.getLastVisibleIndex(); 377 378 if (current != last) { 379 listBox.setSelectedIndex(last); 380 return; 381 } 382 383 final int page = listBox.getVisibleRect().height / listBox.getCellBounds(0, 0).height; 384 final int end = listBox.getModel().getSize() - 1; 385 int target = last + page; 386 if (target > end) target = end; 387 388 listBox.ensureIndexIsVisible(target); 389 listBox.setSelectedIndex(target); 390 } 391 }; 392 393 Action hideAction = new ComboBoxAction() { 394 @Override 395 void performComboBoxAction(final AquaComboBoxUI ui) { 396 comboBox.firePopupMenuCanceled(); 397 comboBox.setPopupVisible(false); 398 } 399 }; 400 401 // For <rdar://problem/3759984> Java 1.4.2_5: Serializing Swing components not working 402 // Inner classes were using a this reference and then trying to serialize the AquaComboBoxUI 403 // We shouldn't do that. But we need to be able to get the popup from other classes, so we need 404 // a public accessor. 405 public ComboPopup getPopup() { 406 return popup; 407 } 408 409 protected LayoutManager createLayoutManager() { 410 return new AquaComboBoxLayoutManager(); 411 } 412 413 class AquaComboBoxLayoutManager extends BasicComboBoxUI.ComboBoxLayoutManager { 414 public void layoutContainer(final Container parent) { 415 if (arrowButton != null && !comboBox.isEditable()) { 416 final Insets insets = comboBox.getInsets(); 417 final int width = comboBox.getWidth(); 418 final int height = comboBox.getHeight(); 419 arrowButton.setBounds(insets.left, insets.top, width - (insets.left + insets.right), height - (insets.top + insets.bottom)); 420 return; 421 } 422 423 final JComboBox cb = (JComboBox)parent; 424 final int width = cb.getWidth(); 425 final int height = cb.getHeight(); 426 427 final Insets insets = getInsets(); 428 final int buttonHeight = height - (insets.top + insets.bottom); 429 final int buttonWidth = 20; 430 431 if (arrowButton != null) { 432 arrowButton.setBounds(width - (insets.right + buttonWidth), insets.top, buttonWidth, buttonHeight); 433 } 434 435 if (editor != null) { 436 final Rectangle editorRect = rectangleForCurrentValue(); 437 editorRect.width += 4; 438 editor.setBounds(editorRect); 439 } 440 } 441 } 442 443 // This is here because Sun can't use protected like they should! 444 protected static final String IS_TABLE_CELL_EDITOR = "JComboBox.isTableCellEditor"; 445 446 protected static boolean isTableCellEditor(final JComponent c) { 447 return Boolean.TRUE.equals(c.getClientProperty(AquaComboBoxUI.IS_TABLE_CELL_EDITOR)); 448 } 449 450 protected static boolean isPopdown(final JComboBox c) { 451 return c.isEditable() || Boolean.TRUE.equals(c.getClientProperty(AquaComboBoxUI.POPDOWN_CLIENT_PROPERTY_KEY)); 452 } 453 454 protected static void triggerSelectionEvent(final JComboBox comboBox, final ActionEvent e) { 455 if (!comboBox.isEnabled()) return; 456 457 final AquaComboBoxUI aquaUi = (AquaComboBoxUI)comboBox.getUI(); 458 459 if (aquaUi.getPopup().getList().getSelectedIndex() < 0) { 460 comboBox.setPopupVisible(false); 461 } 462 463 if (isTableCellEditor(comboBox)) { 464 // Forces the selection of the list item if the combo box is in a JTable 465 comboBox.setSelectedIndex(aquaUi.getPopup().getList().getSelectedIndex()); 466 return; 467 } 468 469 if (comboBox.isPopupVisible()) { 470 comboBox.setSelectedIndex(aquaUi.getPopup().getList().getSelectedIndex()); 471 comboBox.setPopupVisible(false); 472 return; 473 } 474 475 // Call the default button binding. 476 // This is a pretty messy way of passing an event through to the root pane 477 final JRootPane root = SwingUtilities.getRootPane(comboBox); 478 if (root == null) return; 479 480 final InputMap im = root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); 481 final ActionMap am = root.getActionMap(); 482 if (im == null || am == null) return; 483 484 final Object obj = im.get(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0)); 485 if (obj == null) return; 486 487 final Action action = am.get(obj); 488 if (action == null) return; 489 490 action.actionPerformed(new ActionEvent(root, e.getID(), e.getActionCommand(), e.getWhen(), e.getModifiers())); 491 } 492 493 // This is somewhat messy. The difference here from BasicComboBoxUI.EnterAction is that 494 // arrow up or down does not automatically select the 495 static final Action triggerSelectionAction = new AbstractAction() { 496 public void actionPerformed(final ActionEvent e) { 497 triggerSelectionEvent((JComboBox)e.getSource(), e); 498 } 499 }; 500 501 static final Action toggleSelectionAction = new AbstractAction() { 502 public void actionPerformed(final ActionEvent e) { 503 final JComboBox comboBox = (JComboBox)e.getSource(); 504 if (!comboBox.isEnabled()) return; 505 if (comboBox.isEditable()) return; 506 507 final AquaComboBoxUI aquaUi = (AquaComboBoxUI)comboBox.getUI(); 508 509 if (comboBox.isPopupVisible()) { 510 comboBox.setSelectedIndex(aquaUi.getPopup().getList().getSelectedIndex()); 511 comboBox.setPopupVisible(false); 512 return; 513 } 514 515 comboBox.setPopupVisible(true); 516 } 517 }; 518 519 public void applySizeFor(final JComponent c, final Size size) { 520 if (arrowButton == null) return; 521 final Border border = arrowButton.getBorder(); 522 if (!(border instanceof AquaButtonBorder)) return; 523 final AquaButtonBorder aquaBorder = (AquaButtonBorder)border; 524 arrowButton.setBorder(aquaBorder.deriveBorderForSize(size)); 525 } 526 527 public Dimension getMinimumSize(final JComponent c) { 528 if (!isMinimumSizeDirty) { 529 return new Dimension(cachedMinimumSize); 530 } 531 532 final boolean editable = comboBox.isEditable(); 533 534 final Dimension size; 535 if (!editable && arrowButton != null && arrowButton instanceof AquaComboBoxButton) { 536 final AquaComboBoxButton button = (AquaComboBoxButton)arrowButton; 537 final Insets buttonInsets = button.getInsets(); 538 // Insets insets = comboBox.getInsets(); 539 final Insets insets = new Insets(0, 5, 0, 25);//comboBox.getInsets(); 540 541 size = getDisplaySize(); 542 size.width += insets.left + insets.right; 543 size.width += buttonInsets.left + buttonInsets.right; 544 size.width += buttonInsets.right + 10; 545 size.height += insets.top + insets.bottom; 546 size.height += buttonInsets.top + buttonInsets.bottom; 547 // Min height = Height of arrow button plus 2 pixels fuzz above plus 2 below. 23 + 2 + 2 548 size.height = Math.max(27, size.height); 549 } else if (editable && arrowButton != null && editor != null) { 550 size = super.getMinimumSize(c); 551 final Insets margin = arrowButton.getMargin(); 552 size.height += margin.top + margin.bottom; 553 } else { 554 size = super.getMinimumSize(c); 555 } 556 557 final Border border = c.getBorder(); 558 if (border != null) { 559 final Insets insets = border.getBorderInsets(c); 560 size.height += insets.top + insets.bottom; 561 size.width += insets.left + insets.right; 562 } 563 564 cachedMinimumSize.setSize(size.width, size.height); 565 isMinimumSizeDirty = false; 566 567 return new Dimension(cachedMinimumSize); 568 } 569 570 @SuppressWarnings("unchecked") 571 static final RecyclableSingleton<ClientPropertyApplicator<JComboBox, AquaComboBoxUI>> APPLICATOR = new RecyclableSingleton<ClientPropertyApplicator<JComboBox, AquaComboBoxUI>>() { 572 @Override 573 protected ClientPropertyApplicator<JComboBox, AquaComboBoxUI> getInstance() { 574 return new ClientPropertyApplicator<JComboBox, AquaComboBoxUI>( 575 new Property<AquaComboBoxUI>(AquaFocusHandler.FRAME_ACTIVE_PROPERTY) { 576 public void applyProperty(final AquaComboBoxUI target, final Object value) { 577 if (Boolean.FALSE.equals(value)) { 578 if (target.comboBox != null) target.comboBox.hidePopup(); 579 } 580 if (target.listBox != null) target.listBox.repaint(); 581 } 582 }, 583 new Property<AquaComboBoxUI>("editable") { 584 public void applyProperty(final AquaComboBoxUI target, final Object value) { 585 if (target.comboBox == null) return; 586 target.comboBox.repaint(); 587 } 588 }, 589 new Property<AquaComboBoxUI>("background") { 590 public void applyProperty(final AquaComboBoxUI target, final Object value) { 591 final Color color = (Color)value; 592 if (target.arrowButton != null) target.arrowButton.setBackground(color); 593 if (target.listBox != null) target.listBox.setBackground(color); 594 } 595 }, 596 new Property<AquaComboBoxUI>("foreground") { 597 public void applyProperty(final AquaComboBoxUI target, final Object value) { 598 final Color color = (Color)value; 599 if (target.arrowButton != null) target.arrowButton.setForeground(color); 600 if (target.listBox != null) target.listBox.setForeground(color); 601 } 602 }, 603 new Property<AquaComboBoxUI>(POPDOWN_CLIENT_PROPERTY_KEY) { 604 public void applyProperty(final AquaComboBoxUI target, final Object value) { 605 if (!(target.arrowButton instanceof AquaComboBoxButton)) return; 606 ((AquaComboBoxButton)target.arrowButton).setIsPopDown(Boolean.TRUE.equals(value)); 607 } 608 }, 609 new Property<AquaComboBoxUI>(ISSQUARE_CLIENT_PROPERTY_KEY) { 610 public void applyProperty(final AquaComboBoxUI target, final Object value) { 611 if (!(target.arrowButton instanceof AquaComboBoxButton)) return; 612 ((AquaComboBoxButton)target.arrowButton).setIsSquare(Boolean.TRUE.equals(value)); 613 } 614 } 615 ) { 616 public AquaComboBoxUI convertJComponentToTarget(final JComboBox combo) { 617 final ComboBoxUI comboUI = combo.getUI(); 618 if (comboUI instanceof AquaComboBoxUI) return (AquaComboBoxUI)comboUI; 619 return null; 620 } 621 }; 622 } 623 }; 624 static ClientPropertyApplicator<JComboBox, AquaComboBoxUI> getApplicator() { 625 return APPLICATOR.get(); 626 } 627 }