/*
 * Copyright (c) 2011, 2023, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.apple.laf;

import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyEvent;

import javax.accessibility.Accessible;
import javax.accessibility.AccessibleContext;
import javax.accessibility.AccessibleState;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.ComboBoxEditor;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JList;
import javax.swing.JRootPane;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.ListCellRenderer;
import javax.swing.ListModel;
import javax.swing.LookAndFeel;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.plaf.ActionMapUIResource;
import javax.swing.plaf.ComboBoxUI;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.ListUI;
import javax.swing.plaf.UIResource;
import javax.swing.plaf.basic.BasicComboBoxEditor;
import javax.swing.plaf.basic.BasicComboBoxUI;
import javax.swing.plaf.basic.ComboPopup;

import apple.laf.JRSUIConstants.Size;
import com.apple.laf.AquaUtilControlSize.Sizeable;
import com.apple.laf.AquaUtils.RecyclableSingleton;
import com.apple.laf.ClientPropertyApplicator.Property;

// Inspired by MetalComboBoxUI, which also has a combined text-and-arrow button for noneditables
public class AquaComboBoxUI extends BasicComboBoxUI implements Sizeable {
    static final String POPDOWN_CLIENT_PROPERTY_KEY = "JComboBox.isPopDown";
    static final String ISSQUARE_CLIENT_PROPERTY_KEY = "JComboBox.isSquare";

    public static ComponentUI createUI(final JComponent c) {
        return new AquaComboBoxUI();
    }

    private boolean wasOpaque;
    public void installUI(final JComponent c) {
        super.installUI(c);

        // this doesn't work right now, because the JComboBox.init() method calls
        // .setOpaque(false) directly, and doesn't allow the LaF to decided. Bad Sun!
        LookAndFeel.installProperty(c, "opaque", Boolean.FALSE);

        wasOpaque = c.isOpaque();
        c.setOpaque(false);
    }

    public void uninstallUI(final JComponent c) {
        c.setOpaque(wasOpaque);
        super.uninstallUI(c);
    }

    protected void installListeners() {
        super.installListeners();
        AquaUtilControlSize.addSizePropertyListener(comboBox);
    }

    protected void uninstallListeners() {
        AquaUtilControlSize.removeSizePropertyListener(comboBox);
        super.uninstallListeners();
    }

    protected void installComponents() {
        super.installComponents();

        // client properties must be applied after the components have been installed,
        // because isSquare and isPopdown are applied to the installed button
        getApplicator().attachAndApplyClientProperties(comboBox);
    }

    protected void uninstallComponents() {
        getApplicator().removeFrom(comboBox);
        // AquaButtonUI install some listeners to all parents, which means that
        // we need to uninstall UI here to remove those listeners, because after
        // we remove them from ComboBox we lost the latest reference to them,
        // and our standard uninstallUI machinery will not call them.
        arrowButton.getUI().uninstallUI(arrowButton);
        super.uninstallComponents();
    }

    protected ItemListener createItemListener() {
        return new ItemListener() {
            long lastBlink = 0L;
            public void itemStateChanged(final ItemEvent e) {
                if (e.getStateChange() != ItemEvent.SELECTED) return;
                if (!popup.isVisible()) return;

                // sometimes, multiple selection changes can occur while the popup is up,
                // and blinking more than "once" (in a second) is not desirable
                final long now = System.currentTimeMillis();
                if (now - 1000 < lastBlink) return;
                lastBlink = now;

                final JList<Object> itemList = popup.getList();
                final ListUI listUI = itemList.getUI();
                if (!(listUI instanceof AquaListUI)) return;
                final AquaListUI aquaListUI = (AquaListUI)listUI;

                final int selectedIndex = comboBox.getSelectedIndex();
                final ListModel<Object> dataModel = itemList.getModel();
                if (dataModel == null) return;

                final Object value = dataModel.getElementAt(selectedIndex);
                AquaUtils.blinkMenu(new AquaUtils.Selectable() {
                    public void paintSelected(final boolean selected) {
                        aquaListUI.repaintCell(value, selectedIndex, selected);
                    }
                });
            }
        };
    }

    public void paint(final Graphics g, final JComponent c) {
        // this space intentionally left blank
    }

    protected ListCellRenderer<Object> createRenderer() {
        return new AquaComboBoxRenderer(comboBox);
    }

    protected ComboPopup createPopup() {
        return new AquaComboBoxPopup(comboBox);
    }

    protected JButton createArrowButton() {
        return new AquaComboBoxButton(this, comboBox, currentValuePane, listBox);
    }

    protected ComboBoxEditor createEditor() {
        return new AquaComboBoxEditor();
    }

    final class AquaComboBoxEditor extends BasicComboBoxEditor
            implements UIResource, DocumentListener {

        AquaComboBoxEditor() {
            super();
            editor = new AquaCustomComboTextField();
            editor.addFocusListener(this);
            editor.getDocument().addDocumentListener(this);
        }

        @Override
        public void changedUpdate(final DocumentEvent e) {
            editorTextChanged();
        }

        @Override
        public void insertUpdate(final DocumentEvent e) {
            editorTextChanged();
        }

        @Override
        public void removeUpdate(final DocumentEvent e) {
            editorTextChanged();
        }

        private void editorTextChanged() {
            if (!popup.isVisible()) return;

            final Object text = editor.getText();

            final ListModel<Object> model = listBox.getModel();
            final int items = model.getSize();
            for (int i = 0; i < items; i++) {
                final Object element = model.getElementAt(i);
                if (element == null) continue;

                final String asString = element.toString();
                if (asString == null || !asString.equals(text)) continue;

                popup.getList().setSelectedIndex(i);
                return;
            }

            popup.getList().clearSelection();
        }
    }

    @SuppressWarnings("serial") // Superclass is not serializable across versions
    class AquaCustomComboTextField extends JTextField {
        @SuppressWarnings("serial") // anonymous class
        public AquaCustomComboTextField() {
            final InputMap inputMap = getInputMap();
            inputMap.put(KeyStroke.getKeyStroke("DOWN"), highlightNextAction);
            inputMap.put(KeyStroke.getKeyStroke("KP_DOWN"), highlightNextAction);
            inputMap.put(KeyStroke.getKeyStroke("UP"), highlightPreviousAction);
            inputMap.put(KeyStroke.getKeyStroke("KP_UP"), highlightPreviousAction);

            inputMap.put(KeyStroke.getKeyStroke("HOME"), highlightFirstAction);
            inputMap.put(KeyStroke.getKeyStroke("END"), highlightLastAction);
            inputMap.put(KeyStroke.getKeyStroke("PAGE_UP"), highlightPageUpAction);
            inputMap.put(KeyStroke.getKeyStroke("PAGE_DOWN"), highlightPageDownAction);

            final Action action = getActionMap().get(JTextField.notifyAction);
            inputMap.put(KeyStroke.getKeyStroke("ENTER"), new AbstractAction() {
                public void actionPerformed(final ActionEvent e) {
                    if (popup.isVisible()) {
                        triggerSelectionEvent(comboBox, e);

                        if (editor instanceof AquaCustomComboTextField) {
                            ((AquaCustomComboTextField)editor).selectAll();
                        }
                    }
                    action.actionPerformed(e);
                }
            });
        }

        // workaround for 4530952
        public void setText(final String s) {
            if (getText().equals(s)) {
                return;
            }
            super.setText(s);
        }
    }

    /**
     * This listener hides the popup when the focus is lost.  It also repaints
     * when focus is gained or lost.
     *
     * This override is necessary because the Basic L&F for the combo box is working
     * around a Solaris-only bug that we don't have on Mac OS X.  So, remove the lightweight
     * popup check here. rdar://Problem/3518582
     */
    protected FocusListener createFocusListener() {
        return new BasicComboBoxUI.FocusHandler() {
            @Override
            public void focusGained(FocusEvent e) {
                super.focusGained(e);

                if (arrowButton != null) {
                    arrowButton.repaint();
                }
            }

            @Override
            public void focusLost(final FocusEvent e) {
                hasFocus = false;
                if (!e.isTemporary()) {
                    setPopupVisible(comboBox, false);
                }
                comboBox.repaint();

                // Notify assistive technologies that the combo box lost focus
                final AccessibleContext ac = ((Accessible)comboBox).getAccessibleContext();
                if (ac != null) {
                    ac.firePropertyChange(AccessibleContext.ACCESSIBLE_STATE_PROPERTY, AccessibleState.FOCUSED, null);
                }

                if (arrowButton != null) {
                    arrowButton.repaint();
                }
            }
        };
    }

    protected void installKeyboardActions() {
        super.installKeyboardActions();

        ActionMap actionMap = new ActionMapUIResource();

        actionMap.put("aquaSelectNext", highlightNextAction);
        actionMap.put("aquaSelectPrevious", highlightPreviousAction);
        actionMap.put("enterPressed", triggerSelectionAction);
        actionMap.put("aquaSpacePressed", toggleSelectionAction);

        actionMap.put("aquaSelectHome", highlightFirstAction);
        actionMap.put("aquaSelectEnd", highlightLastAction);
        actionMap.put("aquaSelectPageUp", highlightPageUpAction);
        actionMap.put("aquaSelectPageDown", highlightPageDownAction);

        actionMap.put("aquaHidePopup", hideAction);
        actionMap.put("aquaOpenPopupOrhighlightLast", openPopupOrHighlightLast);
        actionMap.put("aquaOpenPopupOrhighlightFirst", openPopupOrHighlightFirst);

        SwingUtilities.replaceUIActionMap(comboBox, actionMap);
    }

    @SuppressWarnings("serial") // Superclass is not serializable across versions
    private abstract class ComboBoxAction extends AbstractAction {
        public void actionPerformed(final ActionEvent e) {
            if (!comboBox.isEnabled() || !comboBox.isShowing()) {
                return;
            }

            if (comboBox.isPopupVisible()) {
                final AquaComboBoxUI ui = (AquaComboBoxUI)comboBox.getUI();
                performComboBoxAction(ui);
            } else {
                comboBox.setPopupVisible(true);
            }
        }

        abstract void performComboBoxAction(final AquaComboBoxUI ui);
    }

    /**
     * Highlight _but do not select_ the next item in the list.
     */
    @SuppressWarnings("serial") // anonymous class
    private Action highlightNextAction = new ComboBoxAction() {
        @Override
        public void performComboBoxAction(AquaComboBoxUI ui) {
            final int si = listBox.getSelectedIndex();

            if (si < comboBox.getModel().getSize() - 1) {
                listBox.setSelectedIndex(si + 1);
                listBox.ensureIndexIsVisible(si + 1);
            }
            comboBox.repaint();
        }
    };

    /**
     * Highlight _but do not select_ the previous item in the list.
     */
    @SuppressWarnings("serial") // anonymous class
    private Action highlightPreviousAction = new ComboBoxAction() {
        @Override
        void performComboBoxAction(final AquaComboBoxUI ui) {
            final int si = listBox.getSelectedIndex();
            if (si > 0) {
                listBox.setSelectedIndex(si - 1);
                listBox.ensureIndexIsVisible(si - 1);
            }
            comboBox.repaint();
        }
    };

    @SuppressWarnings("serial") // anonymous class
    private Action highlightFirstAction = new ComboBoxAction() {
        @Override
        void performComboBoxAction(final AquaComboBoxUI ui) {
            listBox.setSelectedIndex(0);
            listBox.ensureIndexIsVisible(0);
        }
    };

    @SuppressWarnings("serial") // anonymous class
    private Action highlightLastAction = new ComboBoxAction() {
        @Override
        void performComboBoxAction(final AquaComboBoxUI ui) {
            final int size = listBox.getModel().getSize();
            listBox.setSelectedIndex(size - 1);
            listBox.ensureIndexIsVisible(size - 1);
        }
    };

    @SuppressWarnings("serial") // anonymous class
    private Action highlightPageUpAction = new ComboBoxAction() {
        @Override
        void performComboBoxAction(final AquaComboBoxUI ui) {
            final int current = listBox.getSelectedIndex();
            final int first = listBox.getFirstVisibleIndex();

            if (current != first) {
                listBox.setSelectedIndex(first);
                return;
            }

            final int page = listBox.getVisibleRect().height / listBox.getCellBounds(0, 0).height;
            int target = first - page;
            if (target < 0) target = 0;

            listBox.ensureIndexIsVisible(target);
            listBox.setSelectedIndex(target);
        }
    };

    @SuppressWarnings("serial") // anonymous class
    private Action highlightPageDownAction = new ComboBoxAction() {
        @Override
        void performComboBoxAction(final AquaComboBoxUI ui) {
            final int current = listBox.getSelectedIndex();
            final int last = listBox.getLastVisibleIndex();

            if (current != last) {
                listBox.setSelectedIndex(last);
                return;
            }

            final int page = listBox.getVisibleRect().height / listBox.getCellBounds(0, 0).height;
            final int end = listBox.getModel().getSize() - 1;
            int target = last + page;
            if (target > end) target = end;

            listBox.ensureIndexIsVisible(target);
            listBox.setSelectedIndex(target);
        }
    };

    // For <rdar://problem/3759984> Java 1.4.2_5: Serializing Swing components not working
    // Inner classes were using a this reference and then trying to serialize the AquaComboBoxUI
    // We shouldn't do that. But we need to be able to get the popup from other classes, so we need
    // a public accessor.
    public ComboPopup getPopup() {
        return popup;
    }

    protected LayoutManager createLayoutManager() {
        return new AquaComboBoxLayoutManager();
    }

    class AquaComboBoxLayoutManager extends BasicComboBoxUI.ComboBoxLayoutManager {
        protected Rectangle rectangleForCurrentValue() {
            int width = comboBox.getWidth();
            int height = comboBox.getBorder() == null ? 22 : comboBox.getHeight();
            Insets insets = getInsets();
            int buttonSize = height - (insets.top + insets.bottom);
            if ( arrowButton != null )  {
                buttonSize = arrowButton.getWidth();
            }
            int midHeight = (comboBox.getHeight() - height - (insets.top + insets.bottom)) / 2 - 1;
            if (midHeight < 0) {
                midHeight = 0;
            }

            if (comboBox.getComponentOrientation().isLeftToRight()) {
                return new Rectangle(insets.left, insets.top + midHeight,
                        width - (insets.left + insets.right + buttonSize) + 3,
                        height - (insets.top + insets.bottom));
            }
            else {
                return new Rectangle(insets.left + buttonSize, insets.top + midHeight,
                        width - (insets.left + insets.right + buttonSize) + 3,
                        height - (insets.top + insets.bottom));
            }
        }

        public void layoutContainer(final Container parent) {
            if (arrowButton != null && !comboBox.isEditable()) {
                final Insets insets = comboBox.getInsets();
                final int width = comboBox.getWidth();
                final int height = comboBox.getHeight();
                arrowButton.setBounds(insets.left, insets.top, width - (insets.left + insets.right), height - (insets.top + insets.bottom));
                return;
            }

            final JComboBox<?> cb = (JComboBox<?>) parent;
            final int width = cb.getWidth();
            final int height = cb.getHeight();

            final Insets insets = getInsets();
            final int buttonHeight = height - (insets.top + insets.bottom);
            final int buttonWidth = 20;

            if (arrowButton != null) {
                arrowButton.setBounds(width - (insets.right + buttonWidth), insets.top, buttonWidth, buttonHeight);
            }

            if (editor != null) {
                final Rectangle editorRect = rectangleForCurrentValue();
                editor.setBounds(editorRect);
            }
        }
    }

    // This is here because Sun can't use protected like they should!
    protected static final String IS_TABLE_CELL_EDITOR = "JComboBox.isTableCellEditor";

    protected static boolean isTableCellEditor(final JComponent c) {
        return Boolean.TRUE.equals(c.getClientProperty(AquaComboBoxUI.IS_TABLE_CELL_EDITOR));
    }

    protected static boolean isPopdown(final JComboBox<?> c) {
        return c.isEditable() || Boolean.TRUE.equals(c.getClientProperty(AquaComboBoxUI.POPDOWN_CLIENT_PROPERTY_KEY));
    }

    protected static void triggerSelectionEvent(final JComboBox<?> comboBox, final ActionEvent e) {
        if (!comboBox.isEnabled()) return;

        final AquaComboBoxUI aquaUi = (AquaComboBoxUI)comboBox.getUI();

        if (aquaUi.getPopup().getList().getSelectedIndex() < 0) {
            comboBox.setPopupVisible(false);
        }

        if (isTableCellEditor(comboBox)) {
            // Forces the selection of the list item if the combo box is in a JTable
            comboBox.setSelectedIndex(aquaUi.getPopup().getList().getSelectedIndex());
            return;
        }

        if (comboBox.isPopupVisible()) {
            comboBox.setSelectedIndex(aquaUi.getPopup().getList().getSelectedIndex());
            comboBox.setPopupVisible(false);
            return;
        }

        // Call the default button binding.
        // This is a pretty messy way of passing an event through to the root pane
        final JRootPane root = SwingUtilities.getRootPane(comboBox);
        if (root == null) return;

        final InputMap im = root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
        final ActionMap am = root.getActionMap();
        if (im == null || am == null) return;

        final Object obj = im.get(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0));
        if (obj == null) return;

        final Action action = am.get(obj);
        if (action == null) return;

        action.actionPerformed(new ActionEvent(root, e.getID(), e.getActionCommand(), e.getWhen(), e.getModifiers()));
    }

    // This is somewhat messy.  The difference here from BasicComboBoxUI.EnterAction is that
    // arrow up or down does not automatically select the
    @SuppressWarnings("serial") // anonymous class
    private final Action triggerSelectionAction = new AbstractAction() {
        public void actionPerformed(final ActionEvent e) {
            triggerSelectionEvent((JComboBox)e.getSource(), e);
        }

        @Override
        public boolean isEnabled() {
            return comboBox.isPopupVisible() && super.isEnabled();
        }
    };

    @SuppressWarnings("serial") // anonymous class
    private static final Action toggleSelectionAction = new AbstractAction() {
        public void actionPerformed(final ActionEvent e) {
            final JComboBox<?> comboBox = (JComboBox<?>) e.getSource();
            if (!comboBox.isEnabled()) return;
            if (comboBox.isEditable()) return;

            final AquaComboBoxUI aquaUi = (AquaComboBoxUI)comboBox.getUI();

            if (comboBox.isPopupVisible()) {
                comboBox.setSelectedIndex(aquaUi.getPopup().getList().getSelectedIndex());
                comboBox.setPopupVisible(false);
                return;
            }

            comboBox.setPopupVisible(true);
        }
    };

    @SuppressWarnings("serial") // anonymous class
    private final Action hideAction = new AbstractAction() {
        @Override
        public void actionPerformed(final ActionEvent e) {
            final JComboBox<?> comboBox = (JComboBox<?>) e.getSource();
            comboBox.firePopupMenuCanceled();
            comboBox.setPopupVisible(false);
        }

        @Override
        public boolean isEnabled() {
            return comboBox.isPopupVisible() && super.isEnabled();
        }
    };

    @SuppressWarnings("serial") // anonymous class
    private final Action openPopupOrHighlightLast = new ComboBoxAction() {
        @Override
        void performComboBoxAction(final AquaComboBoxUI ui) {
            final int size = listBox.getModel().getSize();
            listBox.setSelectedIndex(size - 1);
            listBox.ensureIndexIsVisible(size - 1);
            comboBox.setSelectedIndex(ui.getPopup().getList().getSelectedIndex());
        }
    };

    @SuppressWarnings("serial") // anonymous class
    private final Action openPopupOrHighlightFirst = new ComboBoxAction() {
        @Override
        void performComboBoxAction(final AquaComboBoxUI ui) {
           listBox.setSelectedIndex(0);
           listBox.ensureIndexIsVisible(0);
           comboBox.setSelectedIndex(ui.getPopup().getList().getSelectedIndex());
        }
    };

    public void applySizeFor(final JComponent c, final Size size) {
        if (arrowButton == null) return;
        final Border border = arrowButton.getBorder();
        if (!(border instanceof AquaButtonBorder)) return;
        final AquaButtonBorder aquaBorder = (AquaButtonBorder)border;
        arrowButton.setBorder(aquaBorder.deriveBorderForSize(size));
    }

    public Dimension getMinimumSize(final JComponent c) {
        if (!isMinimumSizeDirty) {
            return new Dimension(cachedMinimumSize);
        }

        final boolean editable = comboBox.isEditable();

        final Dimension size;
        if (!editable && arrowButton instanceof final AquaComboBoxButton button) {
            final Insets buttonInsets = button.getInsets();
            //  Insets insets = comboBox.getInsets();
            final Insets insets = new Insets(0, 5, 0, 25);//comboBox.getInsets();

            size = getDisplaySize();
            size.width += insets.left + insets.right;
            size.width += buttonInsets.left + buttonInsets.right;
            size.width += buttonInsets.right + 10;
            size.height += insets.top + insets.bottom;
            size.height += buttonInsets.top + buttonInsets.bottom;
            // Min height = Height of arrow button plus 2 pixels fuzz above plus 2 below.  23 + 2 + 2
            size.height = Math.max(27, size.height);
        } else if (editable && arrowButton != null && editor != null) {
            size = super.getMinimumSize(c);
            final Insets margin = arrowButton.getMargin();
            size.height += margin.top + margin.bottom;
        } else {
            size = super.getMinimumSize(c);
        }

        cachedMinimumSize.setSize(size.width, size.height);
        isMinimumSizeDirty = false;

        return new Dimension(cachedMinimumSize);
    }

    @SuppressWarnings("unchecked")
    private static final RecyclableSingleton<ClientPropertyApplicator<JComboBox<?>, AquaComboBoxUI>> APPLICATOR = new
            RecyclableSingleton<ClientPropertyApplicator<JComboBox<?>, AquaComboBoxUI>>() {
        @Override
        protected ClientPropertyApplicator<JComboBox<?>, AquaComboBoxUI> getInstance() {
            return new ClientPropertyApplicator<JComboBox<?>, AquaComboBoxUI>(
                new Property<AquaComboBoxUI>(AquaFocusHandler.FRAME_ACTIVE_PROPERTY) {
                    public void applyProperty(final AquaComboBoxUI target, final Object value) {
                        if (Boolean.FALSE.equals(value)) {
                            if (target.comboBox != null) target.comboBox.hidePopup();
                        }
                        if (target.listBox != null) target.listBox.repaint();
                    }
                },
                new Property<AquaComboBoxUI>("editable") {
                    public void applyProperty(final AquaComboBoxUI target, final Object value) {
                        if (target.comboBox == null) return;
                        target.comboBox.repaint();
                    }
                },
                new Property<AquaComboBoxUI>("background") {
                    public void applyProperty(final AquaComboBoxUI target, final Object value) {
                        final Color color = (Color)value;
                        if (target.arrowButton != null) target.arrowButton.setBackground(color);
                        if (target.listBox != null) target.listBox.setBackground(color);
                    }
                },
                new Property<AquaComboBoxUI>("foreground") {
                    public void applyProperty(final AquaComboBoxUI target, final Object value) {
                        final Color color = (Color)value;
                        if (target.arrowButton != null) target.arrowButton.setForeground(color);
                        if (target.listBox != null) target.listBox.setForeground(color);
                    }
                },
                new Property<AquaComboBoxUI>(POPDOWN_CLIENT_PROPERTY_KEY) {
                    public void applyProperty(final AquaComboBoxUI target, final Object value) {
                        if (!(target.arrowButton instanceof AquaComboBoxButton)) return;
                        ((AquaComboBoxButton)target.arrowButton).setIsPopDown(Boolean.TRUE.equals(value));
                    }
                },
                new Property<AquaComboBoxUI>(ISSQUARE_CLIENT_PROPERTY_KEY) {
                    public void applyProperty(final AquaComboBoxUI target, final Object value) {
                        if (!(target.arrowButton instanceof AquaComboBoxButton)) return;
                        ((AquaComboBoxButton)target.arrowButton).setIsSquare(Boolean.TRUE.equals(value));
                    }
                }
            ) {
                public AquaComboBoxUI convertJComponentToTarget(final JComboBox<?> combo) {
                    final ComboBoxUI comboUI = combo.getUI();
                    if (comboUI instanceof AquaComboBoxUI) return (AquaComboBoxUI)comboUI;
                    return null;
                }
            };
        }
    };
    static ClientPropertyApplicator<JComboBox<?>, AquaComboBoxUI> getApplicator() {
        return APPLICATOR.get();
    }

    @Override
    public int getAccessibleChildrenCount(JComponent c) {
        return 2;
    }

    @Override
    public Accessible getAccessibleChild(JComponent c, int i) {
        // 0 = the popup
        // 1 = the editor for editable combobox and the arrow button for non-editable combobox
        switch ( i ) {
            case 0:
                if (popup instanceof Accessible accessiblePopup) {
                    AccessibleContext ac = accessiblePopup.getAccessibleContext();
                    ac.setAccessibleParent(comboBox);
                    return accessiblePopup;
                }
                break;
            case 1:
                if (comboBox.isEditable()
                        && (editor instanceof Accessible accessibleEditor)) {
                    AccessibleContext ac = accessibleEditor.getAccessibleContext();
                    ac.setAccessibleParent(comboBox);
                    return accessibleEditor;
                } else if (!comboBox.isEditable()) {
                    return arrowButton;
                }
                break;
        }
        return null;
    }
}
