All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.pushingpixels.substance.internal.ui.SubstanceComboBoxUI Maven / Gradle / Ivy

There is a newer version: 4.5.0
Show newest version
/*
 * Copyright (c) 2005-2020 Radiance Kirill Grouchnikov. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of the copyright holder nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.pushingpixels.substance.internal.ui;

import org.pushingpixels.neon.api.NeonCortex;
import org.pushingpixels.substance.api.renderer.SubstanceDefaultComboBoxRenderer;
import org.pushingpixels.substance.internal.SubstanceSynapse;
import org.pushingpixels.substance.internal.animation.StateTransitionTracker;
import org.pushingpixels.substance.internal.animation.TransitionAwareUI;
import org.pushingpixels.substance.internal.utils.*;
import org.pushingpixels.substance.internal.utils.SubstanceCoreUtilities.TextComponentAware;
import org.pushingpixels.substance.internal.utils.border.SubstanceTextComponentBorder;
import org.pushingpixels.substance.internal.utils.combo.ComboBoxBackgroundDelegate;
import org.pushingpixels.substance.internal.utils.combo.SubstanceComboBoxEditor;
import org.pushingpixels.substance.internal.utils.combo.SubstanceComboPopup;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.BorderUIResource;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.UIResource;
import javax.swing.plaf.basic.BasicComboBoxUI;
import javax.swing.plaf.basic.ComboPopup;
import javax.swing.text.JTextComponent;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;

/**
 * UI for combo boxes in Substance  look and feel.
 * 
 * @author Kirill Grouchnikov
 * @author Thomas Bierhance http://www.orbital-computer.de/JComboBox/
 * @author inostock
 */
public class SubstanceComboBoxUI extends BasicComboBoxUI implements TransitionAwareUI {
    /**
     * Property change handler.
     */
    private ComboBoxPropertyChangeHandler substanceChangeHandler;

    private StateTransitionTracker stateTransitionTracker;

    /**
     * Surrogate button model for tracking the state transitions.
     */
    private ButtonModel transitionModel;

    /**
     * Listener for transition animations.
     */
    private RolloverTextControlListener substanceRolloverListener;

    /**
     * Painting delegate.
     */
    private ComboBoxBackgroundDelegate delegate;

    private Icon uneditableArrowIcon;

    private Insets layoutInsets;

    @SuppressWarnings("unchecked")
    public static ComponentUI createUI(JComponent comp) {
        SubstanceCoreUtilities.testComponentCreationThreadingViolation(comp);
        SubstanceComboBoxUI ui = new SubstanceComboBoxUI((JComboBox) comp);
        ui.comboBox = (JComboBox) comp;
        ui.comboBox.putClientProperty(SubstanceButtonUI.OPACITY_ORIGINAL, ui.comboBox.isOpaque());
        ui.comboBox.setOpaque(false);
        return ui;
    }

    @Override
    public void installUI(JComponent c) {
        super.installUI(c);

        c.putClientProperty(SubstanceCoreUtilities.TEXT_COMPONENT_AWARE,
                (TextComponentAware>) t -> {
                    if (t.isEditable()) {
                        Component editorComp = t.getEditor().getEditorComponent();
                        if (editorComp instanceof JTextComponent) {
                            return (JTextComponent) editorComp;
                        }
                    }
                    return null;
                });
    }

    @Override
    public void uninstallUI(JComponent c) {
        c.putClientProperty(SubstanceCoreUtilities.TEXT_COMPONENT_AWARE, null);
        Object originalOpacity = c.getClientProperty(SubstanceButtonUI.OPACITY_ORIGINAL);
        if (originalOpacity instanceof Boolean) {
            c.setOpaque((Boolean) originalOpacity);
        }
        c.putClientProperty(SubstanceButtonUI.OPACITY_ORIGINAL, null);

        super.uninstallUI(c);
    }

    private SubstanceComboBoxUI(JComboBox combo) {
        this.comboBox = combo;

        this.transitionModel = new DefaultButtonModel();
        this.transitionModel.setArmed(false);
        this.transitionModel.setSelected(false);
        this.transitionModel.setPressed(false);
        this.transitionModel.setRollover(false);
        this.transitionModel.setEnabled(combo.isEnabled());

        this.stateTransitionTracker = new StateTransitionTracker(this.comboBox,
                this.transitionModel);

        this.delegate = new ComboBoxBackgroundDelegate();
    }

    @Override
    protected JButton createArrowButton() {
        SubstanceDropDownButton result = new SubstanceDropDownButton(this.comboBox);
        result.setFont(this.comboBox.getFont());
        result.setIcon(getCurrentIcon(result));
        return result;
    }

    /**
     * Returns the icon for the specified arrow button.
     * 
     * @param button
     *            Arrow button.
     * @return Icon for the specified button.
     */
    private Icon getCurrentIcon(JButton button) {
        int popupFlyoutOrientation = SubstanceCoreUtilities.getPopupFlyoutOrientation(
                this.comboBox);
        return SubstanceCoreUtilities.getArrowIcon(button, popupFlyoutOrientation);
    }

    @Override
    protected ListCellRenderer createRenderer() {
        return new SubstanceDefaultComboBoxRenderer.SubstanceUIResource(this.comboBox);
    }

    @Override
    protected void installListeners() {
        super.installListeners();
        this.substanceChangeHandler = new ComboBoxPropertyChangeHandler();
        this.comboBox.addPropertyChangeListener(this.substanceChangeHandler);

        this.substanceRolloverListener = new RolloverTextControlListener(this.comboBox, this,
                this.transitionModel);
        this.substanceRolloverListener.registerListeners();

        this.stateTransitionTracker.registerModelListeners();
        this.stateTransitionTracker.registerFocusListeners();
    }

    @Override
    protected void uninstallListeners() {
        this.stateTransitionTracker.unregisterModelListeners();
        this.stateTransitionTracker.unregisterFocusListeners();

        this.comboBox.removePropertyChangeListener(this.substanceChangeHandler);
        this.substanceChangeHandler = null;

        this.substanceRolloverListener.unregisterListeners();
        this.substanceRolloverListener = null;

        super.uninstallListeners();
    }

    @Override
    protected void installDefaults() {
        super.installDefaults();

        // this icon must be created after the font has been installed
        // on the combobox
        this.uneditableArrowIcon = SubstanceCoreUtilities.getArrowIcon(this.comboBox,
                () -> (TransitionAwareUI) comboBox.getUI(),
                SubstanceCoreUtilities.getPopupFlyoutOrientation(this.comboBox));
        this.updateComboBoxBorder();
    }

    @Override
    protected LayoutManager createLayoutManager() {
        return new SubstanceComboBoxLayoutManager();
    }

    /**
     * Layout manager for combo box.
     * 
     * @author Kirill Grouchnikov
     */
    private class SubstanceComboBoxLayoutManager extends BasicComboBoxUI.ComboBoxLayoutManager {
        @Override
        public void layoutContainer(Container parent) {
            JComboBox cb = (JComboBox) parent;

            int width = cb.getWidth();
            int height = cb.getHeight();

            Insets insets = layoutInsets;
            int buttonWidth = (comboBox.isEditable())
                    ? SubstanceSizeUtils
                            .getScrollBarWidth(SubstanceSizeUtils.getComponentFontSize(comboBox))
                    : uneditableArrowIcon.getIconWidth();

            if (arrowButton != null) {
                if (!comboBox.isEditable()) {
                    arrowButton.setBounds(0, 0, 0, 0);
                } else {
                    if (cb.getComponentOrientation().isLeftToRight()) {
                        arrowButton.setBounds(width - buttonWidth - insets.right, 0,
                                buttonWidth + insets.right, height);
                    } else {
                        arrowButton.setBounds(0, 0, buttonWidth + insets.left, height);
                    }
                }
            }
            if (editor != null) {
                Rectangle r = rectangleForCurrentValue();
                editor.setBounds(r);
            }
        }
    }

    @Override
    protected Rectangle rectangleForCurrentValue() {
        int width = this.comboBox.getWidth();
        int height = this.comboBox.getHeight();
        Insets insets = this.layoutInsets;
        int buttonWidth = SubstanceSizeUtils
                .getScrollBarWidth(SubstanceSizeUtils.getComponentFontSize(comboBox));
        if (this.comboBox.getComponentOrientation().isLeftToRight()) {
            return new Rectangle(insets.left, insets.top,
                    width - insets.left - insets.right - buttonWidth,
                    height - insets.top - insets.bottom);
        } else {
            int startX = insets.left + buttonWidth;
            return new Rectangle(startX, insets.top, width - startX - insets.right,
                    height - insets.top - insets.bottom);
        }
    }

    @Override
    protected Dimension getDefaultSize() {
        Component rend = new SubstanceDefaultComboBoxRenderer(this.comboBox)
                .getListCellRendererComponent(listBox, " ", -1, false, false);
        rend.setFont(this.comboBox.getFont());

        return rend.getPreferredSize();
    }

    @Override
    public Dimension getMinimumSize(JComponent c) {
//        if (!this.isMinimumSizeDirty) {
//            return new Dimension(this.cachedMinimumSize);
//        }

        // Dimension size = null;
        //
        // if (!this.comboBox.isEditable() && this.arrowButton != null
        // && this.arrowButton instanceof SubstanceComboBoxButton) {
        //
        SubstanceDropDownButton button = (SubstanceDropDownButton) this.arrowButton;
        Insets buttonInsets = button.getInsets();
        Insets insets = this.comboBox.getInsets();

        Dimension size = this.getDisplaySize();

        size.width += insets.left + insets.right;
        size.width += buttonInsets.left + buttonInsets.right;
        size.width += button.getMinimumSize().getWidth();
        size.height += insets.top + insets.bottom;
        // } else if (this.comboBox.isEditable() && this.arrowButton != null
        // && this.editor != null) {
        // size = super.getMinimumSize(c);
        // } else {
        // size = super.getMinimumSize(c);
        // }

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

        return new Dimension(this.cachedMinimumSize);
    }

    /**
     * This property change handler changes combo box arrow icon based on the
     * enabled status of the combo box.
     * 
     * @author Kirill Grouchnikov
     */
    public class ComboBoxPropertyChangeHandler extends PropertyChangeHandler {
        @Override
        public void propertyChange(final PropertyChangeEvent e) {
            String propertyName = e.getPropertyName();

            if (propertyName.equals("componentOrientation")) {
                SwingUtilities.invokeLater(() -> {
                    if (SubstanceComboBoxUI.this.comboBox == null)
                        return;
                    final ComponentOrientation newOrientation = (ComponentOrientation) e
                            .getNewValue();
                    final ListCellRenderer cellRenderer = SubstanceComboBoxUI.this.comboBox
                            .getRenderer();
                    final ComboBoxEditor editor = SubstanceComboBoxUI.this.comboBox.getEditor();
                    if (SubstanceComboBoxUI.this.popup instanceof Component) {
                        final Component cPopup = (Component) SubstanceComboBoxUI.this.popup;
                        cPopup.applyComponentOrientation(newOrientation);
                        cPopup.doLayout();
                    }
                    if (cellRenderer instanceof Component) {
                        ((Component) cellRenderer).applyComponentOrientation(newOrientation);
                    }
                    if ((editor != null) && (editor.getEditorComponent() != null)) {
                        (editor.getEditorComponent()).applyComponentOrientation(newOrientation);
                    }
                    if (SubstanceComboBoxUI.this.comboBox != null)
                        SubstanceComboBoxUI.this.comboBox.repaint();
                });
            }

            if (SubstanceSynapse.COMBO_BOX_POPUP_FLYOUT_ORIENTATION.equals(propertyName)) {
                SubstanceDropDownButton dropDownButton = (SubstanceDropDownButton) arrowButton;
                dropDownButton.setIcon(getCurrentIcon(dropDownButton));
                uneditableArrowIcon = SubstanceCoreUtilities.getArrowIcon(comboBox,
                        () -> (TransitionAwareUI) comboBox.getUI(),
                        SubstanceCoreUtilities.getPopupFlyoutOrientation(comboBox));
            }

            if ("font".equals(propertyName)) {
                SwingUtilities.invokeLater(() -> {
                    if (comboBox != null) {
                        comboBox.updateUI();
                    }
                });
            }

            if ("background".equals(propertyName)) {
                if (comboBox.isEditable()) {
                    comboBox.getEditor().getEditorComponent()
                            .setBackground(comboBox.getBackground());
                    popup.getList().setBackground(comboBox.getBackground());
                }
            }

            if ("editable".equals(propertyName)) {
                updateComboBoxBorder();
                isMinimumSizeDirty = true;
            }

            if ("enabled".equals(propertyName)) {
                SubstanceComboBoxUI.this.transitionModel.setEnabled(comboBox.isEnabled());
            }
            // Do not call super - fix for bug 63
        }
    }

    @Override
    protected ComboPopup createPopup() {
        final SubstanceComboPopup sPopup = new SubstanceComboPopup(this.comboBox);
        final ComponentOrientation currOrientation = this.comboBox.getComponentOrientation();

        SwingUtilities.invokeLater(() -> {
            if (SubstanceComboBoxUI.this.comboBox == null) {
                return;
            }

            sPopup.applyComponentOrientation(currOrientation);
            sPopup.doLayout();
            ListCellRenderer cellRenderer = SubstanceComboBoxUI.this.comboBox.getRenderer();
            if (cellRenderer instanceof Component) {
                ((Component) cellRenderer).applyComponentOrientation(currOrientation);
            }
            ComboBoxEditor editor = SubstanceComboBoxUI.this.comboBox.getEditor();
            if ((editor != null) && (editor.getEditorComponent() != null)) {
                (editor.getEditorComponent()).applyComponentOrientation(currOrientation);
            }
            SubstanceComboBoxUI.this.comboBox.repaint();
        });
        return sPopup;
    }

    @Override
    public void paint(Graphics g, JComponent c) {
        Graphics2D graphics = (Graphics2D) g.create();
        int width = this.comboBox.getWidth();
        int height = this.comboBox.getHeight();
        Insets insets = this.comboBox.getInsets();

        int componentFontSize = SubstanceSizeUtils.getComponentFontSize(this.comboBox);
        if (this.comboBox.isEditable()) {
            SubstanceTextUtilities.paintTextCompBackground(g, c);
        } else {
            this.delegate.updateBackground(graphics, this.comboBox, this.transitionModel);

            Icon icon = this.uneditableArrowIcon;
            int iw = icon.getIconWidth();
            int ih = icon.getIconHeight();
            int origButtonWidth = SubstanceSizeUtils.getScrollBarWidth(componentFontSize);
            Graphics2D forIcon = (Graphics2D) graphics.create();
            forIcon.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                    RenderingHints.VALUE_INTERPOLATION_BICUBIC);
            int iconY = 1 + insets.top + (height - insets.top - insets.bottom - ih) / 2;
            if (this.comboBox.getComponentOrientation().isLeftToRight()) {
                int iconX = width - origButtonWidth - insets.right / 2 + (origButtonWidth - iw) / 2;
                forIcon.translate(iconX, iconY);
                icon.paintIcon(this.comboBox, forIcon, 0, 0);
            } else {
                int iconX = insets.left / 2 + (origButtonWidth - iw) / 2;
                forIcon.translate(iconX, iconY);
                icon.paintIcon(this.comboBox, forIcon, 0, 0);
            }
            forIcon.dispose();
        }

        hasFocus = comboBox.hasFocus();
        if (!comboBox.isEditable()) {
            Rectangle r = rectangleForCurrentValue();

            ListCellRenderer renderer = this.comboBox.getRenderer();
            Component rendererComponent;
            if (hasFocus) {
                rendererComponent = renderer.getListCellRendererComponent(this.listBox,
                        this.comboBox.getSelectedItem(), -1, true, hasFocus);
            } else {
                rendererComponent = renderer.getListCellRendererComponent(this.listBox,
                        this.comboBox.getSelectedItem(), -1, false, hasFocus);
            }
            rendererComponent.setFont(this.comboBox.getFont());

            // Fix for 4238829: should lay out the JPanel.
            boolean shouldValidate = false;
            if (rendererComponent instanceof JPanel) {
                shouldValidate = true;
            }

            if (this.comboBox.getComponentOrientation().isLeftToRight()) {
                this.currentValuePane.paintComponent(graphics, rendererComponent, this.comboBox,
                        r.x, r.y, r.width, r.height, shouldValidate);
            } else {
                this.currentValuePane.paintComponent(graphics, rendererComponent, this.comboBox,
                        r.x, r.y, r.width, r.height, shouldValidate);
            }
        }

        if (!this.comboBox.isEditable()) {
            Rectangle r = new Rectangle(insets.left, layoutInsets.top,
                    width - insets.left - insets.right,
                    height - layoutInsets.top - layoutInsets.bottom);
            this.paintFocus(graphics, r);
        }

        graphics.dispose();
    }

    /**
     * Paints the focus indication.
     * 
     * @param g
     *            Graphics.
     * @param bounds
     *            Bounds for text.
     */
    private void paintFocus(Graphics g, Rectangle bounds) {
        int fontSize = SubstanceSizeUtils.getComponentFontSize(this.comboBox);
        int x = bounds.x;
        int y = bounds.y;
        Graphics2D g2d = (Graphics2D) g.create();
        g2d.translate(x, y);

        SubstanceCoreUtilities.paintFocus(g2d, this.comboBox, this.comboBox, this,
                SubstanceOutlineUtilities.getBaseOutline(bounds.width, bounds.height,
                        SubstanceSizeUtils.getClassicButtonCornerRadius(fontSize), null, 0),
                bounds, 1.0f, 0.0f);

        g2d.dispose();
    }

    /**
     * Returns the popup of the associated combobox.
     * 
     * @return The popup of the associated combobox.
     */
    public ComboPopup getPopup() {
        return this.popup;
    }

    @Override
    public void configureArrowButton() {
        super.configureArrowButton();
        // Mustang decided to make the arrow button focusable on
        // focusable comboboxes
        this.arrowButton.setFocusable(false);
    }

    @Override
    protected void configureEditor() {
        super.configureEditor();
        // This for Mustang - setting Substance once again adds a border on
        // the text field in the combo editor.
        if (this.editor instanceof JComponent) {
            Insets ins = SubstanceSizeUtils
                    .getComboTextBorderInsets(SubstanceSizeUtils.getComponentFontSize(this.editor));
            ((JComponent) this.editor)
                    .setBorder(new EmptyBorder(ins.top, ins.left, ins.bottom, ins.right));
            this.editor.setBackground(this.comboBox.getBackground());
            // ((JComponent) this.editor).setBorder(new LineBorder(Color.red));
        }
    }

    @Override
    protected ComboBoxEditor createEditor() {
        return new SubstanceComboBoxEditor.UIResource();
    }

    private void updateComboBoxBorder() {
        Border b = this.comboBox.getBorder();
        if (b == null || b instanceof UIResource) {
            int comboFontSize = SubstanceSizeUtils.getComponentFontSize(this.comboBox);
            Insets comboBorderInsets = SubstanceSizeUtils.getComboBorderInsets(comboFontSize);
            if (this.comboBox.isEditable()) {
                SubstanceTextComponentBorder border = new SubstanceTextComponentBorder(
                        comboBorderInsets);
                this.comboBox.setBorder(border);
            } else {
                this.comboBox
                        .setBorder(new BorderUIResource.EmptyBorderUIResource(comboBorderInsets));
                // BasicComboBoxUI does not invalidate display size when
                // combo becomes uneditable. However, this is not good
                // in Substance which has different preferred size for
                // editable and uneditable combos. Calling the method below
                // will trigger the path in BasicComboBoxUI.Handler that
                // will invalidate the cached sizes.
                this.comboBox.setPrototypeDisplayValue(this.comboBox.getPrototypeDisplayValue());
            }
            this.layoutInsets = SubstanceSizeUtils.getComboLayoutInsets(comboFontSize);
        } else {
            this.layoutInsets = new Insets(0, 0, 0, 0);
        }
    }

    @Override
    public StateTransitionTracker getTransitionTracker() {
        return this.stateTransitionTracker;
    }

    @Override
    public boolean isInside(MouseEvent me) {
        if (!SubstanceCoreUtilities.isCurrentLookAndFeel()) {
            return false;
        }
        Shape contour = SubstanceOutlineUtilities.getBaseOutline(this.comboBox,
                SubstanceSizeUtils.getClassicButtonCornerRadius(
                        SubstanceSizeUtils.getComponentFontSize(this.comboBox)),
                null);
        return contour.contains(me.getPoint());
    }

    @Override
    public void update(Graphics g, JComponent c) {
        Graphics2D g2d = (Graphics2D) g.create();
        NeonCortex.installDesktopHints(g2d, c.getFont());
        this.paint(g2d, c);
        g2d.dispose();
    }

    @Override
    public Dimension getPreferredSize(JComponent c) {
        return super.getPreferredSize(c);
    }
}