Path: blob/master/src/java.desktop/macosx/classes/com/apple/laf/AquaComboBoxUI.java
41154 views
/*1* Copyright (c) 2011, 2020, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425package com.apple.laf;2627import java.awt.Color;28import java.awt.Container;29import java.awt.Dimension;30import java.awt.Graphics;31import java.awt.Insets;32import java.awt.LayoutManager;33import java.awt.Rectangle;34import java.awt.event.ActionEvent;35import java.awt.event.FocusEvent;36import java.awt.event.FocusListener;37import java.awt.event.ItemEvent;38import java.awt.event.ItemListener;39import java.awt.event.KeyEvent;4041import javax.accessibility.Accessible;42import javax.accessibility.AccessibleContext;43import javax.accessibility.AccessibleState;44import javax.swing.AbstractAction;45import javax.swing.Action;46import javax.swing.ActionMap;47import javax.swing.ComboBoxEditor;48import javax.swing.InputMap;49import javax.swing.JButton;50import javax.swing.JComboBox;51import javax.swing.JComponent;52import javax.swing.JList;53import javax.swing.JRootPane;54import javax.swing.JTextField;55import javax.swing.KeyStroke;56import javax.swing.ListCellRenderer;57import javax.swing.ListModel;58import javax.swing.LookAndFeel;59import javax.swing.SwingUtilities;60import javax.swing.border.Border;61import javax.swing.event.DocumentEvent;62import javax.swing.event.DocumentListener;63import javax.swing.plaf.ActionMapUIResource;64import javax.swing.plaf.ComboBoxUI;65import javax.swing.plaf.ComponentUI;66import javax.swing.plaf.ListUI;67import javax.swing.plaf.UIResource;68import javax.swing.plaf.basic.BasicComboBoxEditor;69import javax.swing.plaf.basic.BasicComboBoxUI;70import javax.swing.plaf.basic.ComboPopup;7172import apple.laf.JRSUIConstants.Size;73import com.apple.laf.AquaUtilControlSize.Sizeable;74import com.apple.laf.AquaUtils.RecyclableSingleton;75import com.apple.laf.ClientPropertyApplicator.Property;7677// Inspired by MetalComboBoxUI, which also has a combined text-and-arrow button for noneditables78public class AquaComboBoxUI extends BasicComboBoxUI implements Sizeable {79static final String POPDOWN_CLIENT_PROPERTY_KEY = "JComboBox.isPopDown";80static final String ISSQUARE_CLIENT_PROPERTY_KEY = "JComboBox.isSquare";8182public static ComponentUI createUI(final JComponent c) {83return new AquaComboBoxUI();84}8586private boolean wasOpaque;87public void installUI(final JComponent c) {88super.installUI(c);8990// this doesn't work right now, because the JComboBox.init() method calls91// .setOpaque(false) directly, and doesn't allow the LaF to decided. Bad Sun!92LookAndFeel.installProperty(c, "opaque", Boolean.FALSE);9394wasOpaque = c.isOpaque();95c.setOpaque(false);96}9798public void uninstallUI(final JComponent c) {99c.setOpaque(wasOpaque);100super.uninstallUI(c);101}102103protected void installListeners() {104super.installListeners();105AquaUtilControlSize.addSizePropertyListener(comboBox);106}107108protected void uninstallListeners() {109AquaUtilControlSize.removeSizePropertyListener(comboBox);110super.uninstallListeners();111}112113protected void installComponents() {114super.installComponents();115116// client properties must be applied after the components have been installed,117// because isSquare and isPopdown are applied to the installed button118getApplicator().attachAndApplyClientProperties(comboBox);119}120121protected void uninstallComponents() {122getApplicator().removeFrom(comboBox);123// AquaButtonUI install some listeners to all parents, which means that124// we need to uninstall UI here to remove those listeners, because after125// we remove them from ComboBox we lost the latest reference to them,126// and our standard uninstallUI machinery will not call them.127arrowButton.getUI().uninstallUI(arrowButton);128super.uninstallComponents();129}130131protected ItemListener createItemListener() {132return new ItemListener() {133long lastBlink = 0L;134public void itemStateChanged(final ItemEvent e) {135if (e.getStateChange() != ItemEvent.SELECTED) return;136if (!popup.isVisible()) return;137138// sometimes, multiple selection changes can occur while the popup is up,139// and blinking more than "once" (in a second) is not desirable140final long now = System.currentTimeMillis();141if (now - 1000 < lastBlink) return;142lastBlink = now;143144final JList<Object> itemList = popup.getList();145final ListUI listUI = itemList.getUI();146if (!(listUI instanceof AquaListUI)) return;147final AquaListUI aquaListUI = (AquaListUI)listUI;148149final int selectedIndex = comboBox.getSelectedIndex();150final ListModel<Object> dataModel = itemList.getModel();151if (dataModel == null) return;152153final Object value = dataModel.getElementAt(selectedIndex);154AquaUtils.blinkMenu(new AquaUtils.Selectable() {155public void paintSelected(final boolean selected) {156aquaListUI.repaintCell(value, selectedIndex, selected);157}158});159}160};161}162163public void paint(final Graphics g, final JComponent c) {164// this space intentionally left blank165}166167protected ListCellRenderer<Object> createRenderer() {168return new AquaComboBoxRenderer(comboBox);169}170171protected ComboPopup createPopup() {172return new AquaComboBoxPopup(comboBox);173}174175protected JButton createArrowButton() {176return new AquaComboBoxButton(this, comboBox, currentValuePane, listBox);177}178179protected ComboBoxEditor createEditor() {180return new AquaComboBoxEditor();181}182183final class AquaComboBoxEditor extends BasicComboBoxEditor184implements UIResource, DocumentListener {185186AquaComboBoxEditor() {187super();188editor = new AquaCustomComboTextField();189editor.addFocusListener(this);190editor.getDocument().addDocumentListener(this);191}192193@Override194public void changedUpdate(final DocumentEvent e) {195editorTextChanged();196}197198@Override199public void insertUpdate(final DocumentEvent e) {200editorTextChanged();201}202203@Override204public void removeUpdate(final DocumentEvent e) {205editorTextChanged();206}207208private void editorTextChanged() {209if (!popup.isVisible()) return;210211final Object text = editor.getText();212213final ListModel<Object> model = listBox.getModel();214final int items = model.getSize();215for (int i = 0; i < items; i++) {216final Object element = model.getElementAt(i);217if (element == null) continue;218219final String asString = element.toString();220if (asString == null || !asString.equals(text)) continue;221222popup.getList().setSelectedIndex(i);223return;224}225226popup.getList().clearSelection();227}228}229230@SuppressWarnings("serial") // Superclass is not serializable across versions231class AquaCustomComboTextField extends JTextField {232@SuppressWarnings("serial") // anonymous class233public AquaCustomComboTextField() {234final InputMap inputMap = getInputMap();235inputMap.put(KeyStroke.getKeyStroke("DOWN"), highlightNextAction);236inputMap.put(KeyStroke.getKeyStroke("KP_DOWN"), highlightNextAction);237inputMap.put(KeyStroke.getKeyStroke("UP"), highlightPreviousAction);238inputMap.put(KeyStroke.getKeyStroke("KP_UP"), highlightPreviousAction);239240inputMap.put(KeyStroke.getKeyStroke("HOME"), highlightFirstAction);241inputMap.put(KeyStroke.getKeyStroke("END"), highlightLastAction);242inputMap.put(KeyStroke.getKeyStroke("PAGE_UP"), highlightPageUpAction);243inputMap.put(KeyStroke.getKeyStroke("PAGE_DOWN"), highlightPageDownAction);244245final Action action = getActionMap().get(JTextField.notifyAction);246inputMap.put(KeyStroke.getKeyStroke("ENTER"), new AbstractAction() {247public void actionPerformed(final ActionEvent e) {248if (popup.isVisible()) {249triggerSelectionEvent(comboBox, e);250251if (editor instanceof AquaCustomComboTextField) {252((AquaCustomComboTextField)editor).selectAll();253}254} else {255action.actionPerformed(e);256}257}258});259}260261// workaround for 4530952262public void setText(final String s) {263if (getText().equals(s)) {264return;265}266super.setText(s);267}268}269270/**271* This listener hides the popup when the focus is lost. It also repaints272* when focus is gained or lost.273*274* This override is necessary because the Basic L&F for the combo box is working275* around a Solaris-only bug that we don't have on Mac OS X. So, remove the lightweight276* popup check here. rdar://Problem/3518582277*/278protected FocusListener createFocusListener() {279return new BasicComboBoxUI.FocusHandler() {280@Override281public void focusGained(FocusEvent e) {282super.focusGained(e);283284if (arrowButton != null) {285arrowButton.repaint();286}287}288289@Override290public void focusLost(final FocusEvent e) {291hasFocus = false;292if (!e.isTemporary()) {293setPopupVisible(comboBox, false);294}295comboBox.repaint();296297// Notify assistive technologies that the combo box lost focus298final AccessibleContext ac = ((Accessible)comboBox).getAccessibleContext();299if (ac != null) {300ac.firePropertyChange(AccessibleContext.ACCESSIBLE_STATE_PROPERTY, AccessibleState.FOCUSED, null);301}302303if (arrowButton != null) {304arrowButton.repaint();305}306}307};308}309310protected void installKeyboardActions() {311super.installKeyboardActions();312313ActionMap actionMap = new ActionMapUIResource();314315actionMap.put("aquaSelectNext", highlightNextAction);316actionMap.put("aquaSelectPrevious", highlightPreviousAction);317actionMap.put("enterPressed", triggerSelectionAction);318actionMap.put("aquaSpacePressed", toggleSelectionAction);319320actionMap.put("aquaSelectHome", highlightFirstAction);321actionMap.put("aquaSelectEnd", highlightLastAction);322actionMap.put("aquaSelectPageUp", highlightPageUpAction);323actionMap.put("aquaSelectPageDown", highlightPageDownAction);324325actionMap.put("aquaHidePopup", hideAction);326327SwingUtilities.replaceUIActionMap(comboBox, actionMap);328}329330@SuppressWarnings("serial") // Superclass is not serializable across versions331private abstract class ComboBoxAction extends AbstractAction {332public void actionPerformed(final ActionEvent e) {333if (!comboBox.isEnabled() || !comboBox.isShowing()) {334return;335}336337if (comboBox.isPopupVisible()) {338final AquaComboBoxUI ui = (AquaComboBoxUI)comboBox.getUI();339performComboBoxAction(ui);340} else {341comboBox.setPopupVisible(true);342}343}344345abstract void performComboBoxAction(final AquaComboBoxUI ui);346}347348/**349* Hilight _but do not select_ the next item in the list.350*/351@SuppressWarnings("serial") // anonymous class352private Action highlightNextAction = new ComboBoxAction() {353@Override354public void performComboBoxAction(AquaComboBoxUI ui) {355final int si = listBox.getSelectedIndex();356357if (si < comboBox.getModel().getSize() - 1) {358listBox.setSelectedIndex(si + 1);359listBox.ensureIndexIsVisible(si + 1);360}361comboBox.repaint();362}363};364365/**366* Hilight _but do not select_ the previous item in the list.367*/368@SuppressWarnings("serial") // anonymous class369private Action highlightPreviousAction = new ComboBoxAction() {370@Override371void performComboBoxAction(final AquaComboBoxUI ui) {372final int si = listBox.getSelectedIndex();373if (si > 0) {374listBox.setSelectedIndex(si - 1);375listBox.ensureIndexIsVisible(si - 1);376}377comboBox.repaint();378}379};380381@SuppressWarnings("serial") // anonymous class382private Action highlightFirstAction = new ComboBoxAction() {383@Override384void performComboBoxAction(final AquaComboBoxUI ui) {385listBox.setSelectedIndex(0);386listBox.ensureIndexIsVisible(0);387}388};389390@SuppressWarnings("serial") // anonymous class391private Action highlightLastAction = new ComboBoxAction() {392@Override393void performComboBoxAction(final AquaComboBoxUI ui) {394final int size = listBox.getModel().getSize();395listBox.setSelectedIndex(size - 1);396listBox.ensureIndexIsVisible(size - 1);397}398};399400@SuppressWarnings("serial") // anonymous class401private Action highlightPageUpAction = new ComboBoxAction() {402@Override403void performComboBoxAction(final AquaComboBoxUI ui) {404final int current = listBox.getSelectedIndex();405final int first = listBox.getFirstVisibleIndex();406407if (current != first) {408listBox.setSelectedIndex(first);409return;410}411412final int page = listBox.getVisibleRect().height / listBox.getCellBounds(0, 0).height;413int target = first - page;414if (target < 0) target = 0;415416listBox.ensureIndexIsVisible(target);417listBox.setSelectedIndex(target);418}419};420421@SuppressWarnings("serial") // anonymous class422private Action highlightPageDownAction = new ComboBoxAction() {423@Override424void performComboBoxAction(final AquaComboBoxUI ui) {425final int current = listBox.getSelectedIndex();426final int last = listBox.getLastVisibleIndex();427428if (current != last) {429listBox.setSelectedIndex(last);430return;431}432433final int page = listBox.getVisibleRect().height / listBox.getCellBounds(0, 0).height;434final int end = listBox.getModel().getSize() - 1;435int target = last + page;436if (target > end) target = end;437438listBox.ensureIndexIsVisible(target);439listBox.setSelectedIndex(target);440}441};442443// For <rdar://problem/3759984> Java 1.4.2_5: Serializing Swing components not working444// Inner classes were using a this reference and then trying to serialize the AquaComboBoxUI445// We shouldn't do that. But we need to be able to get the popup from other classes, so we need446// a public accessor.447public ComboPopup getPopup() {448return popup;449}450451protected LayoutManager createLayoutManager() {452return new AquaComboBoxLayoutManager();453}454455class AquaComboBoxLayoutManager extends BasicComboBoxUI.ComboBoxLayoutManager {456public void layoutContainer(final Container parent) {457if (arrowButton != null && !comboBox.isEditable()) {458final Insets insets = comboBox.getInsets();459final int width = comboBox.getWidth();460final int height = comboBox.getHeight();461arrowButton.setBounds(insets.left, insets.top, width - (insets.left + insets.right), height - (insets.top + insets.bottom));462return;463}464465final JComboBox<?> cb = (JComboBox<?>) parent;466final int width = cb.getWidth();467final int height = cb.getHeight();468469final Insets insets = getInsets();470final int buttonHeight = height - (insets.top + insets.bottom);471final int buttonWidth = 20;472473if (arrowButton != null) {474arrowButton.setBounds(width - (insets.right + buttonWidth), insets.top, buttonWidth, buttonHeight);475}476477if (editor != null) {478final Rectangle editorRect = rectangleForCurrentValue();479editorRect.width += 4;480editorRect.height += 1;481editor.setBounds(editorRect);482}483}484}485486// This is here because Sun can't use protected like they should!487protected static final String IS_TABLE_CELL_EDITOR = "JComboBox.isTableCellEditor";488489protected static boolean isTableCellEditor(final JComponent c) {490return Boolean.TRUE.equals(c.getClientProperty(AquaComboBoxUI.IS_TABLE_CELL_EDITOR));491}492493protected static boolean isPopdown(final JComboBox<?> c) {494return c.isEditable() || Boolean.TRUE.equals(c.getClientProperty(AquaComboBoxUI.POPDOWN_CLIENT_PROPERTY_KEY));495}496497protected static void triggerSelectionEvent(final JComboBox<?> comboBox, final ActionEvent e) {498if (!comboBox.isEnabled()) return;499500final AquaComboBoxUI aquaUi = (AquaComboBoxUI)comboBox.getUI();501502if (aquaUi.getPopup().getList().getSelectedIndex() < 0) {503comboBox.setPopupVisible(false);504}505506if (isTableCellEditor(comboBox)) {507// Forces the selection of the list item if the combo box is in a JTable508comboBox.setSelectedIndex(aquaUi.getPopup().getList().getSelectedIndex());509return;510}511512if (comboBox.isPopupVisible()) {513comboBox.setSelectedIndex(aquaUi.getPopup().getList().getSelectedIndex());514comboBox.setPopupVisible(false);515return;516}517518// Call the default button binding.519// This is a pretty messy way of passing an event through to the root pane520final JRootPane root = SwingUtilities.getRootPane(comboBox);521if (root == null) return;522523final InputMap im = root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);524final ActionMap am = root.getActionMap();525if (im == null || am == null) return;526527final Object obj = im.get(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0));528if (obj == null) return;529530final Action action = am.get(obj);531if (action == null) return;532533action.actionPerformed(new ActionEvent(root, e.getID(), e.getActionCommand(), e.getWhen(), e.getModifiers()));534}535536// This is somewhat messy. The difference here from BasicComboBoxUI.EnterAction is that537// arrow up or down does not automatically select the538@SuppressWarnings("serial") // anonymous class539private final Action triggerSelectionAction = new AbstractAction() {540public void actionPerformed(final ActionEvent e) {541triggerSelectionEvent((JComboBox)e.getSource(), e);542}543544@Override545public boolean isEnabled() {546return comboBox.isPopupVisible() && super.isEnabled();547}548};549550@SuppressWarnings("serial") // anonymous class551private static final Action toggleSelectionAction = new AbstractAction() {552public void actionPerformed(final ActionEvent e) {553final JComboBox<?> comboBox = (JComboBox<?>) e.getSource();554if (!comboBox.isEnabled()) return;555if (comboBox.isEditable()) return;556557final AquaComboBoxUI aquaUi = (AquaComboBoxUI)comboBox.getUI();558559if (comboBox.isPopupVisible()) {560comboBox.setSelectedIndex(aquaUi.getPopup().getList().getSelectedIndex());561comboBox.setPopupVisible(false);562return;563}564565comboBox.setPopupVisible(true);566}567};568569@SuppressWarnings("serial") // anonymous class570private final Action hideAction = new AbstractAction() {571@Override572public void actionPerformed(final ActionEvent e) {573final JComboBox<?> comboBox = (JComboBox<?>) e.getSource();574comboBox.firePopupMenuCanceled();575comboBox.setPopupVisible(false);576}577578@Override579public boolean isEnabled() {580return comboBox.isPopupVisible() && super.isEnabled();581}582};583584public void applySizeFor(final JComponent c, final Size size) {585if (arrowButton == null) return;586final Border border = arrowButton.getBorder();587if (!(border instanceof AquaButtonBorder)) return;588final AquaButtonBorder aquaBorder = (AquaButtonBorder)border;589arrowButton.setBorder(aquaBorder.deriveBorderForSize(size));590}591592public Dimension getMinimumSize(final JComponent c) {593if (!isMinimumSizeDirty) {594return new Dimension(cachedMinimumSize);595}596597final boolean editable = comboBox.isEditable();598599final Dimension size;600if (!editable && arrowButton != null && arrowButton instanceof AquaComboBoxButton) {601final AquaComboBoxButton button = (AquaComboBoxButton)arrowButton;602final Insets buttonInsets = button.getInsets();603// Insets insets = comboBox.getInsets();604final Insets insets = new Insets(0, 5, 0, 25);//comboBox.getInsets();605606size = getDisplaySize();607size.width += insets.left + insets.right;608size.width += buttonInsets.left + buttonInsets.right;609size.width += buttonInsets.right + 10;610size.height += insets.top + insets.bottom;611size.height += buttonInsets.top + buttonInsets.bottom;612// Min height = Height of arrow button plus 2 pixels fuzz above plus 2 below. 23 + 2 + 2613size.height = Math.max(27, size.height);614} else if (editable && arrowButton != null && editor != null) {615size = super.getMinimumSize(c);616final Insets margin = arrowButton.getMargin();617size.height += margin.top + margin.bottom;618} else {619size = super.getMinimumSize(c);620}621622final Border border = c.getBorder();623if (border != null) {624final Insets insets = border.getBorderInsets(c);625size.height += insets.top + insets.bottom;626size.width += insets.left + insets.right;627}628629cachedMinimumSize.setSize(size.width, size.height);630isMinimumSizeDirty = false;631632return new Dimension(cachedMinimumSize);633}634635@SuppressWarnings("unchecked")636private static final RecyclableSingleton<ClientPropertyApplicator<JComboBox<?>, AquaComboBoxUI>> APPLICATOR = new637RecyclableSingleton<ClientPropertyApplicator<JComboBox<?>, AquaComboBoxUI>>() {638@Override639protected ClientPropertyApplicator<JComboBox<?>, AquaComboBoxUI> getInstance() {640return new ClientPropertyApplicator<JComboBox<?>, AquaComboBoxUI>(641new Property<AquaComboBoxUI>(AquaFocusHandler.FRAME_ACTIVE_PROPERTY) {642public void applyProperty(final AquaComboBoxUI target, final Object value) {643if (Boolean.FALSE.equals(value)) {644if (target.comboBox != null) target.comboBox.hidePopup();645}646if (target.listBox != null) target.listBox.repaint();647}648},649new Property<AquaComboBoxUI>("editable") {650public void applyProperty(final AquaComboBoxUI target, final Object value) {651if (target.comboBox == null) return;652target.comboBox.repaint();653}654},655new Property<AquaComboBoxUI>("background") {656public void applyProperty(final AquaComboBoxUI target, final Object value) {657final Color color = (Color)value;658if (target.arrowButton != null) target.arrowButton.setBackground(color);659if (target.listBox != null) target.listBox.setBackground(color);660}661},662new Property<AquaComboBoxUI>("foreground") {663public void applyProperty(final AquaComboBoxUI target, final Object value) {664final Color color = (Color)value;665if (target.arrowButton != null) target.arrowButton.setForeground(color);666if (target.listBox != null) target.listBox.setForeground(color);667}668},669new Property<AquaComboBoxUI>(POPDOWN_CLIENT_PROPERTY_KEY) {670public void applyProperty(final AquaComboBoxUI target, final Object value) {671if (!(target.arrowButton instanceof AquaComboBoxButton)) return;672((AquaComboBoxButton)target.arrowButton).setIsPopDown(Boolean.TRUE.equals(value));673}674},675new Property<AquaComboBoxUI>(ISSQUARE_CLIENT_PROPERTY_KEY) {676public void applyProperty(final AquaComboBoxUI target, final Object value) {677if (!(target.arrowButton instanceof AquaComboBoxButton)) return;678((AquaComboBoxButton)target.arrowButton).setIsSquare(Boolean.TRUE.equals(value));679}680}681) {682public AquaComboBoxUI convertJComponentToTarget(final JComboBox<?> combo) {683final ComboBoxUI comboUI = combo.getUI();684if (comboUI instanceof AquaComboBoxUI) return (AquaComboBoxUI)comboUI;685return null;686}687};688}689};690static ClientPropertyApplicator<JComboBox<?>, AquaComboBoxUI> getApplicator() {691return APPLICATOR.get();692}693}694695696