/*
 * Copyright (c) 2005-2010 Flamingo 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 Flamingo Kirill Grouchnikov 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.flamingo.internal.ui.common;

import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.ColorConvertOp;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.plaf.*;
import javax.swing.plaf.basic.BasicGraphicsUtils;

import org.pushingpixels.flamingo.api.common.*;
import org.pushingpixels.flamingo.api.common.CommandButtonLayoutManager.CommandButtonLayoutInfo;
import org.pushingpixels.flamingo.api.common.CommandButtonLayoutManager.CommandButtonSeparatorOrientation;
import org.pushingpixels.flamingo.api.common.JCommandButtonStrip.StripOrientation;
import org.pushingpixels.flamingo.api.common.icon.FilteredResizableIcon;
import org.pushingpixels.flamingo.api.common.icon.ResizableIcon;
import org.pushingpixels.flamingo.api.common.model.PopupButtonModel;
import org.pushingpixels.flamingo.api.common.popup.*;
import org.pushingpixels.flamingo.internal.utils.*;

/**
 * Basic UI for command button {@link JCommandButton}.
 *
 * @author Kirill Grouchnikov
 */
public class BasicCommandButtonUI extends CommandButtonUI {
    /**
     * The associated command button.
     */
    protected AbstractCommandButton commandButton;

    /**
     * Indication whether the mouse pointer is over the associated command
     * button.
     */
    protected boolean isUnderMouse;

    /**
     * Property change listener.
     */
    protected PropertyChangeListener propertyChangeListener;

    /**
     * Tracks user interaction with the command button (including keyboard and
     * mouse).
     */
    protected BasicCommandButtonListener basicPopupButtonListener;

    /**
     * Layout information.
     */
    protected CommandButtonLayoutManager.CommandButtonLayoutInfo layoutInfo;

    /**
     * Client property to mark the command button to have square corners. This
     * client property is for internal use only.
     */
    public static final String EMULATE_SQUARE_BUTTON = "flamingo.internal.commandButton.ui.emulateSquare";

    /**
     * Client property to mark the command button to not dispose the popups on
     * activation.
     *
     * @see #disposePopupsActionListener
     */
    public static final String DONT_DISPOSE_POPUPS = "flamingo.internal.commandButton.ui.dontDisposePopups";

    /**
     * This listener disposes all popup panels when button's action is
     * activated. An example of scenario would be a command button in the popup
     * panel of an in-ribbon gallery. When this command button is activated, the
     * associated popup panel is dismissed.
     *
     * @see #DONT_DISPOSE_POPUPS
     */
    protected ActionListener disposePopupsActionListener;

    /**
     * Action listener on the popup area.
     */
    protected PopupActionListener popupActionListener;

    /**
     * The "expand" action icon.
     */
    protected ResizableIcon popupActionIcon;

    protected CommandButtonLayoutManager layoutManager;

    /**
     * Used to provide a LAF-consistent appearance under core LAFs.
     */
    protected CellRendererPane buttonRendererPane;

    /**
     * Used to provide a LAF-consistent appearance under core LAFs.
     */
    protected AbstractButton rendererButton;

    /**
     * Used to paint the separator between the action and popup areas.
     */
    protected JSeparator rendererSeparator;

    /**
     * Internal listener for icon sync,  One copy so we can remove what we add
     */
    private AsynchronousLoadListener resizableIconSyncListener = new AsynchronousLoadListener() {
        @Override
        public void completed(boolean success) {
            if (success) {
                if (commandButton != null) {
                    syncIconDimension();
                    syncDisabledIcon();
                    commandButton.repaint();
                }
            }
        }
    };

    /*
      * (non-Javadoc)
      *
      * @see javax.swing.plaf.ComponentUI#createUI(javax.swing.JComponent)
      */
    public static ComponentUI createUI(JComponent c) {
        return new BasicCommandButtonUI();
    }

    /**
     * Creates a new UI delegate.
     */
    public BasicCommandButtonUI() {
        // this.toTakeSavedDimension = false;
    }

    /*
      * (non-Javadoc)
      *
      * @see javax.swing.plaf.ComponentUI#installUI(javax.swing.JComponent)
      */
    @Override
    public void installUI(JComponent c) {
        this.commandButton = (AbstractCommandButton) c;
        installDefaults();
        installComponents();
        installListeners();
        installKeyboardActions();

        this.layoutManager = this.commandButton.getDisplayState()
                .createLayoutManager(this.commandButton);

        this.updateCustomDimension();
    }

    /*
      * (non-Javadoc)
      *
      * @see javax.swing.plaf.ComponentUI#uninstallUI(javax.swing.JComponent)
      */
    @Override
    public void uninstallUI(JComponent c) {
        c.setLayout(null);

        uninstallKeyboardActions();
        uninstallListeners();
        uninstallComponents();
        uninstallDefaults();
        this.commandButton = null;
    }

    /**
     * Installs defaults on the associated command button.
     */
    protected void installDefaults() {
        configureRenderer();

        this.updateBorder();
        this.syncDisabledIcon();
    }

    protected void configureRenderer() {
        this.buttonRendererPane = new CellRendererPane();
        this.commandButton.add(buttonRendererPane);
        this.rendererButton = createRendererButton();
        this.rendererButton.setOpaque(false);
        this.rendererSeparator = new JSeparator();
        Font currFont = this.commandButton.getFont();
        if ((currFont == null) || (currFont instanceof UIResource)) {
            this.commandButton.setFont(this.rendererButton.getFont());
        }
        // special handling for Mac OS X native look-and-feel
        this.rendererButton.putClientProperty("JButton.buttonType", "square");
    }

    protected void updateBorder() {
        Border currBorder = this.commandButton.getBorder();
        if ((currBorder == null) || (currBorder instanceof UIResource)) {
            int tb = (int) (this.commandButton.getVGapScaleFactor() * 4);
            int lr = (int) (this.commandButton.getHGapScaleFactor() * 6);
            this.commandButton
                    .setBorder(new BorderUIResource.EmptyBorderUIResource(tb,
                            lr, tb, lr));
        }
    }

    /**
     * Creates the renderer button.
     *
     * @return The renderer button.
     */
    protected AbstractButton createRendererButton() {
        return new JButton("");
    }

    /**
     * Installs subcomponents on the associated command button.
     */
    protected void installComponents() {
        this.updatePopupActionIcon();

        ResizableIcon buttonIcon = this.commandButton.getIcon();
        if (buttonIcon instanceof AsynchronousLoading) {
            ((AsynchronousLoading) buttonIcon).addAsynchronousLoadListener(resizableIconSyncListener);
        }

        if (this.commandButton instanceof JCommandButton) {
            this.popupActionIcon = this.createPopupActionIcon();
        }
    }

    /**
     * Installs listeners on the associated command button.
     */
    protected void installListeners() {
        this.basicPopupButtonListener = createButtonListener(this.commandButton);
        if (this.basicPopupButtonListener != null) {
            this.commandButton.addMouseListener(this.basicPopupButtonListener);
            this.commandButton
                    .addMouseMotionListener(this.basicPopupButtonListener);
            this.commandButton.addFocusListener(this.basicPopupButtonListener);
            this.commandButton.addChangeListener(this.basicPopupButtonListener);
        }

        this.propertyChangeListener = new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                if (AbstractButton.ICON_CHANGED_PROPERTY.equals(evt
                        .getPropertyName())) {
                    Icon newIcon = (Icon) evt.getNewValue();
                    if (newIcon instanceof AsynchronousLoading) {
                        AsynchronousLoading async = (AsynchronousLoading) newIcon;
                        async.addAsynchronousLoadListener(resizableIconSyncListener);
                        if (!async.isLoading()) {
                            syncIconDimension();
                            syncDisabledIcon();
                            commandButton.repaint();
                        }
                    } else {
                        syncIconDimension();
                        syncDisabledIcon();
                        commandButton.revalidate();
                        commandButton.repaint();
                    }
                    Icon oldIcon = (Icon) evt.getOldValue();
                    if (oldIcon instanceof AsynchronousLoading) {
                        ((AsynchronousLoading)oldIcon).removeAsynchronousLoadListener(resizableIconSyncListener);
                    }

                }
                if ("commandButtonKind".equals(evt.getPropertyName())) {
                    updatePopupActionIcon();
                }
                if ("popupOrientationKind".equals(evt.getPropertyName())) {
                    updatePopupActionIcon();
                }
                if ("customDimension".equals(evt.getPropertyName())) {
                    updateCustomDimension();
                }
                if ("hgapScaleFactor".equals(evt.getPropertyName())) {
                    updateBorder();
                }
                if ("vgapScaleFactor".equals(evt.getPropertyName())) {
                    updateBorder();
                }

                if ("popupModel".equals(evt.getPropertyName())) {
                    // rewire the popup action listener on the new popup model
                    PopupButtonModel oldModel = (PopupButtonModel) evt
                            .getOldValue();
                    PopupButtonModel newModel = (PopupButtonModel) evt
                            .getNewValue();

                    if (oldModel != null) {
                        oldModel.removePopupActionListener(popupActionListener);
                        popupActionListener = null;
                    }

                    if (newModel != null) {
                        popupActionListener = createPopupActionListener();
                        newModel.addPopupActionListener(popupActionListener);
                    }
                }
                if ("displayState".equals(evt.getPropertyName()) || ("enabled".equals(evt.getPropertyName()) && !((Boolean) evt.getNewValue()))) {
                    syncIconDimension();
                    syncDisabledIcon();

                    commandButton.invalidate();
                    commandButton.revalidate();
                    commandButton.doLayout();
                }

                // pass the event to the layout manager
                if (layoutManager != null) {
                    layoutManager.propertyChange(evt);
                }

                if ("componentOrientation".equals(evt.getPropertyName())) {
                    updatePopupActionIcon();
                    commandButton.repaint();
                }
            }
        };
        this.commandButton
                .addPropertyChangeListener(this.propertyChangeListener);

        this.disposePopupsActionListener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                boolean toDismiss = !Boolean.TRUE.equals(commandButton
                        .getClientProperty(DONT_DISPOSE_POPUPS));
                if (toDismiss) {
                    JCommandPopupMenu menu = (JCommandPopupMenu) SwingUtilities
                            .getAncestorOfClass(JCommandPopupMenu.class,
                                    commandButton);
                    if (menu != null) {
                        toDismiss = menu.isToDismissOnChildClick();
                    }
                }
                if (toDismiss) {
                    if (SwingUtilities.getAncestorOfClass(JPopupPanel.class,
                            commandButton) != null) {
                        SwingUtilities.invokeLater(new Runnable() {
                            @Override
                            public void run() {
                                // command button may be cleared if the
                                // button click resulted in LAF switch
                                if (commandButton != null) {
                                    // clear the active states
                                    commandButton.getActionModel().setPressed(
                                            false);
                                    commandButton.getActionModel().setRollover(
                                            false);
                                    commandButton.getActionModel().setArmed(
                                            false);
                                }
                            }
                        });
                    }
                    PopupPanelManager.defaultManager().hidePopups(null);
                }
            }
        };
        this.commandButton.addActionListener(this.disposePopupsActionListener);

        if (this.commandButton instanceof JCommandButton) {
            this.popupActionListener = this.createPopupActionListener();
            ((JCommandButton) this.commandButton).getPopupModel()
                    .addPopupActionListener(this.popupActionListener);
        }

    }

    /**
     * Creates the icon for the popup area.
     *
     * @return The icon for the popup area.
     */
    protected ResizableIcon createPopupActionIcon() {
        return FlamingoUtilities
                .getCommandButtonPopupActionIcon((JCommandButton) this.commandButton);
    }

    /**
     * Creates the button listener for the specified command button.
     *
     * @param b
     *            Command button.
     * @return The button listener for the specified command button.
     */
    protected BasicCommandButtonListener createButtonListener(
            AbstractCommandButton b) {
        return new BasicCommandButtonListener();
    }

    /**
     * Installs the keyboard actions on the associated command button.
     */
    protected void installKeyboardActions() {
        if (this.basicPopupButtonListener != null) {
            basicPopupButtonListener.installKeyboardActions(this.commandButton);
        }
    }

    /**
     * Uninstalls defaults from the associated command button.
     */
    protected void uninstallDefaults() {
        unconfigureRenderer();
    }

    protected void unconfigureRenderer() {
        if (this.buttonRendererPane != null) {
            this.commandButton.remove(this.buttonRendererPane);
        }
        this.buttonRendererPane = null;
    }

    /**
     * Uninstalls subcomponents from the associated command button.
     */
    protected void uninstallComponents() {
        ResizableIcon buttonIcon = this.commandButton.getIcon();
        if (buttonIcon instanceof AsynchronousLoading) {
            ((AsynchronousLoading) buttonIcon).removeAsynchronousLoadListener(resizableIconSyncListener);
        }
    }

    /**
     * Uninstalls listeners from the associated command button.
     */
    protected void uninstallListeners() {
        if (this.basicPopupButtonListener != null) {
            this.commandButton
                    .removeMouseListener(this.basicPopupButtonListener);
            this.commandButton
                    .removeMouseListener(this.basicPopupButtonListener);
            this.commandButton
                    .removeMouseMotionListener(this.basicPopupButtonListener);
            this.commandButton
                    .removeFocusListener(this.basicPopupButtonListener);
            this.commandButton
                    .removeChangeListener(this.basicPopupButtonListener);
        }

        this.commandButton
                .removePropertyChangeListener(this.propertyChangeListener);
        this.propertyChangeListener = null;

        this.commandButton
                .removeActionListener(this.disposePopupsActionListener);
        this.disposePopupsActionListener = null;

        if (this.commandButton instanceof JCommandButton) {
            ((JCommandButton) this.commandButton).getPopupModel()
                    .removePopupActionListener(this.popupActionListener);
            this.popupActionListener = null;
        }

    }

    /**
     * Uninstalls the keyboard actions from the associated command button.
     */
    protected void uninstallKeyboardActions() {
        if (this.basicPopupButtonListener != null) {
            this.basicPopupButtonListener
                    .uninstallKeyboardActions(this.commandButton);
        }
    }

    /*
      * (non-Javadoc)
      *
      * @see javax.swing.plaf.ComponentUI#update(java.awt.Graphics,
      * javax.swing.JComponent)
      */
    @Override
    public void update(Graphics g, JComponent c) {
        Graphics2D g2d = (Graphics2D) g.create();
        RenderingUtils.installDesktopHints(g2d);
        super.update(g2d, c);
        g2d.dispose();
    }

    /*
      * (non-Javadoc)
      *
      * @see javax.swing.plaf.ComponentUI#paint(java.awt.Graphics,
      * javax.swing.JComponent)
      */
    @Override
    public void paint(Graphics g, JComponent c) {
        g.setFont(FlamingoUtilities.getFont(commandButton, "Ribbon.font",
                "Button.font", "Panel.font"));
        this.layoutInfo = this.layoutManager.getLayoutInfo(this.commandButton,
                g);
        commandButton.putClientProperty("icon.bounds", layoutInfo.iconRect);

        if (this.isPaintingBackground()) {
            this.paintButtonBackground(g, new Rectangle(0, 0, commandButton
                    .getWidth(), commandButton.getHeight()));
        }
        // Graphics2D g2d = (Graphics2D) g.create();
        // g2d.setColor(new Color(255, 0, 0, 64));
        // if (getActionClickArea() != null) {
        // g2d.fill(getActionClickArea());
        // }
        // g2d.setColor(new Color(0, 0, 255, 64));
        // if (getPopupClickArea() != null) {
        // g2d.fill(getPopupClickArea());
        // }
        // g2d.dispose();

        if (layoutInfo.iconRect != null) {
            this.paintButtonIcon(g, layoutInfo.iconRect);
        }
        if (layoutInfo.popupActionRect.getWidth() > 0) {
            paintPopupActionIcon(g, layoutInfo.popupActionRect);
        }
        FontMetrics fm = g.getFontMetrics();

        boolean isTextPaintedEnabled = commandButton.isEnabled();
        if (commandButton instanceof JCommandButton) {
            JCommandButton jCommandButton = (JCommandButton) commandButton;
            isTextPaintedEnabled = layoutInfo.isTextInActionArea ? jCommandButton
                    .getActionModel().isEnabled()
                    : jCommandButton.getPopupModel().isEnabled();
        }

        g.setColor(getForegroundColor(isTextPaintedEnabled));

        if (layoutInfo.textLayoutInfoList != null) {
            for (CommandButtonLayoutManager.TextLayoutInfo mainTextLayoutInfo : layoutInfo.textLayoutInfoList) {
                if (mainTextLayoutInfo.text != null) {
                    BasicGraphicsUtils.drawString(g, mainTextLayoutInfo.text,
                            -1, mainTextLayoutInfo.textRect.x,
                            mainTextLayoutInfo.textRect.y + fm.getAscent());
                }
            }
        }

        if (isTextPaintedEnabled) {
            g.setColor(FlamingoUtilities.getColor(Color.gray,
                    "Label.disabledForeground"));
        } else {
            g.setColor(FlamingoUtilities.getColor(Color.gray,
                    "Label.disabledForeground").brighter());
        }

        if (layoutInfo.extraTextLayoutInfoList != null) {
            for (CommandButtonLayoutManager.TextLayoutInfo extraTextLayoutInfo : layoutInfo.extraTextLayoutInfoList) {
                if (extraTextLayoutInfo.text != null) {
                    BasicGraphicsUtils.drawString(g, extraTextLayoutInfo.text,
                            -1, extraTextLayoutInfo.textRect.x,
                            extraTextLayoutInfo.textRect.y + fm.getAscent());
                }
            }
        }

        if (this.isPaintingSeparators() && (layoutInfo.separatorArea != null)) {
            if (layoutInfo.separatorOrientation == CommandButtonSeparatorOrientation.HORIZONTAL) {
                this
                        .paintButtonHorizontalSeparator(g,
                                layoutInfo.separatorArea);
            } else {
                this.paintButtonVerticalSeparator(g, layoutInfo.separatorArea);
            }
        }

        // Graphics2D g2d = (Graphics2D) g.create();
        //
        // g2d.setColor(Color.red);
        // g2d.draw(layoutInfo.iconRect);
        // g2d.setColor(Color.blue);
        // if (layoutInfo.textLayoutInfoList != null) {
        // for (CommandButtonLayoutManager.TextLayoutInfo mainTextLayoutInfo :
        // layoutInfo.textLayoutInfoList) {
        // if (mainTextLayoutInfo.text != null) {
        // g2d.draw(mainTextLayoutInfo.textRect);
        // }
        // }
        // }
        // g2d.setColor(Color.magenta);
        // if (layoutInfo.extraTextLayoutInfoList != null) {
        // for (CommandButtonLayoutManager.TextLayoutInfo extraTextLayoutInfo :
        // layoutInfo.extraTextLayoutInfoList) {
        // if (extraTextLayoutInfo.text != null) {
        // g2d.draw(extraTextLayoutInfo.textRect);
        // }
        // }
        // }
        // g2d.setColor(Color.green);
        // g2d.draw(layoutInfo.popupActionRect);
        // g2d.dispose();
    }

    protected Color getForegroundColor(boolean isTextPaintedEnabled) {
        if (isTextPaintedEnabled) {
            return FlamingoUtilities.getColor(Color.black, "Button.foreground");
        } else {
            return FlamingoUtilities.getColor(Color.gray,
                    "Label.disabledForeground");
        }
    }

    /**
     * Paints the icon of the popup area.
     *
     * @param g
     *            Graphics context.
     * @param popupActionRect
     */
    protected void paintPopupActionIcon(Graphics g, Rectangle popupActionRect) {
        int size = Math.max(popupActionRect.width - 2, 7);
        if (size % 2 == 0)
            size--;
        popupActionIcon.setDimension(new Dimension(size, size));
        popupActionIcon.paintIcon(this.commandButton, g, popupActionRect.x
                + (popupActionRect.width - size) / 2, popupActionRect.y
                + (popupActionRect.height - size) / 2);
    }

    /**
     * Returns the current icon.
     *
     * @return Current icon.
     */
    protected Icon getIconToPaint() {
        return (toUseDisabledIcon() && this.commandButton.getDisabledIcon() != null) ? this.commandButton
                .getDisabledIcon()
                : this.commandButton.getIcon();
    }

    protected boolean toUseDisabledIcon() {
        // special case for command buttons with POPUP_ONLY kind -
        // check the popup model
        boolean toUseDisabledIcon;
        if (this.commandButton instanceof JCommandButton
                && ((JCommandButton) this.commandButton).getCommandButtonKind() == JCommandButton.CommandButtonKind.POPUP_ONLY) {
            toUseDisabledIcon = !((JCommandButton) this.commandButton)
                    .getPopupModel().isEnabled();
        } else {
            toUseDisabledIcon = !this.commandButton.getActionModel()
                    .isEnabled();
        }
        return toUseDisabledIcon;
    }

    /**
     * Paints command button vertical separator.
     *
     * @param graphics
     *            Graphics context.
     * @param separatorArea
     *            Separator area.
     */
    protected void paintButtonVerticalSeparator(Graphics graphics,
                                                Rectangle separatorArea) {
        this.buttonRendererPane.setBounds(0, 0, this.commandButton.getWidth(),
                this.commandButton.getHeight());
        Graphics2D g2d = (Graphics2D) graphics.create();
        this.rendererSeparator.setOrientation(JSeparator.VERTICAL);
        this.buttonRendererPane.paintComponent(g2d, this.rendererSeparator,
                this.commandButton, separatorArea.x, 2, 2, this.commandButton
                .getHeight() - 4, true);
        g2d.dispose();
    }

    /**
     * Paints command button horizontal separator.
     *
     * @param graphics
     *            Graphics context.
     * @param separatorArea
     *            Separator area.
     */
    protected void paintButtonHorizontalSeparator(Graphics graphics,
                                                  Rectangle separatorArea) {
        this.buttonRendererPane.setBounds(0, 0, this.commandButton.getWidth(),
                this.commandButton.getHeight());
        Graphics2D g2d = (Graphics2D) graphics.create();
        this.rendererSeparator.setOrientation(JSeparator.HORIZONTAL);
        this.buttonRendererPane.paintComponent(g2d, this.rendererSeparator,
                this.commandButton, 2, separatorArea.y, this.commandButton
                .getWidth() - 4, 2, true);
        g2d.dispose();
    }

    /**
     * Paints command button background.
     *
     * @param graphics
     *            Graphics context.
     * @param toFill
     *            Rectangle for the background.
     */
    protected void paintButtonBackground(Graphics graphics, Rectangle toFill) {
        ButtonModel actionModel = this.commandButton.getActionModel();
        PopupButtonModel popupModel = (this.commandButton instanceof JCommandButton) ? ((JCommandButton) this.commandButton)
                .getPopupModel()
                : null;

        // first time - paint the full background passing both models
        this.paintButtonBackground(graphics, toFill, actionModel, popupModel);

        Rectangle actionArea = this.getLayoutInfo().actionClickArea;
        Rectangle popupArea = this.getLayoutInfo().popupClickArea;
        if ((actionArea != null) && !actionArea.isEmpty()) {
            // now overlay the action area with the background matching action
            // model
            Graphics2D graphicsAction = (Graphics2D) graphics.create();
            // System.out.println(actionArea);
            graphicsAction.clip(actionArea);
            float actionAlpha = 0.4f;
            if ((popupModel != null) && !popupModel.isEnabled())
                actionAlpha = 1.0f;
            graphicsAction.setComposite(AlphaComposite.SrcOver
                    .derive(actionAlpha));
            // System.out.println(graphicsAction.getClipBounds());
            this.paintButtonBackground(graphicsAction, toFill, actionModel);
            graphicsAction.dispose();
        }
        if ((popupArea != null) && !popupArea.isEmpty()) {
            // now overlay the popup area with the background matching popup
            // model
            Graphics2D graphicsPopup = (Graphics2D) graphics.create();
            // System.out.println(popupArea);
            graphicsPopup.clip(popupArea);
            // System.out.println(graphicsPopup.getClipBounds());
            float popupAlpha = 0.4f;
            if (!actionModel.isEnabled())
                popupAlpha = 1.0f;
            graphicsPopup.setComposite(AlphaComposite.SrcOver
                    .derive(popupAlpha));
            this.paintButtonBackground(graphicsPopup, toFill, popupModel);
            graphicsPopup.dispose();
        }
    }

    /**
     * Paints the background of the command button.
     *
     * @param graphics
     *            Graphics context.
     * @param toFill
     *            Rectangle to fill.
     * @param modelToUse
     *            Button models to use for computing the background fill.
     */
    protected void paintButtonBackground(Graphics graphics, Rectangle toFill,
                                         ButtonModel... modelToUse) {
        if (modelToUse.length == 0)
            return;
        if ((modelToUse.length == 1) && (modelToUse[0] == null))
            return;

        this.buttonRendererPane.setBounds(toFill.x, toFill.y, toFill.width,
                toFill.height);
        this.rendererButton.setRolloverEnabled(true);
        boolean isEnabled = true;
        boolean isRollover = false;
        boolean isPressed = true;
        boolean isArmed = true;
        boolean isSelected = true;
        for (ButtonModel model : modelToUse) {
            if (model == null)
                continue;
            isEnabled = isEnabled && model.isEnabled();
            isRollover = isRollover || model.isRollover();
            isPressed = isPressed && model.isPressed();
            isArmed = isArmed && model.isArmed();
            isSelected = isSelected && model.isSelected();
            if (model instanceof PopupButtonModel) {
                isRollover = isRollover
                        || ((PopupButtonModel) model).isPopupShowing();
            }
        }
        this.rendererButton.getModel().setEnabled(isEnabled);
        this.rendererButton.getModel().setRollover(isRollover);
        this.rendererButton.getModel().setPressed(isPressed);
        this.rendererButton.getModel().setArmed(isArmed);
        this.rendererButton.getModel().setSelected(isSelected);
        // System.out.println(this.commandButton.getText() + " - e:"
        // + this.rendererButton.getModel().isEnabled() + ", s:"
        // + this.rendererButton.getModel().isSelected() + ", r:"
        // + this.rendererButton.getModel().isRollover() + ", p:"
        // + this.rendererButton.getModel().isPressed() + ", a:"
        // + this.rendererButton.getModel().isArmed());
        Graphics2D g2d = (Graphics2D) graphics.create();

        Color borderColor = FlamingoUtilities.getBorderColor();
        if (Boolean.TRUE.equals(this.commandButton
                .getClientProperty(EMULATE_SQUARE_BUTTON))) {
            this.buttonRendererPane.paintComponent(g2d, this.rendererButton,
                    this.commandButton, toFill.x - toFill.width / 2, toFill.y
                    - toFill.height / 2, 2 * toFill.width,
                    2 * toFill.height, true);
            g2d.setColor(borderColor);
            g2d.drawRect(toFill.x, toFill.y, toFill.width - 1,
                    toFill.height - 1);
        } else {
            AbstractCommandButton.CommandButtonLocationOrderKind locationKind = this.commandButton
                    .getLocationOrderKind();
            Insets outsets = (this.rendererButton instanceof JToggleButton) ? ButtonSizingUtils
                    .getInstance().getToggleOutsets()
                    : ButtonSizingUtils.getInstance().getOutsets();
            if (locationKind != null) {
                if (locationKind == AbstractCommandButton.CommandButtonLocationOrderKind.ONLY) {
                    this.buttonRendererPane.paintComponent(g2d,
                            this.rendererButton, this.commandButton, toFill.x
                            - outsets.left, toFill.y - outsets.top,
                            toFill.width + outsets.left + outsets.right,
                            toFill.height + outsets.top + outsets.bottom, true);
                } else {
                    // special case for parent component which is a vertical
                    // button strip
                    Component parent = this.commandButton.getParent();
                    if ((parent instanceof JCommandButtonStrip)
                            && (((JCommandButtonStrip) parent).getOrientation() == StripOrientation.VERTICAL)) {
                        switch (locationKind) {
                            case FIRST:
                                this.buttonRendererPane.paintComponent(g2d,
                                        this.rendererButton, this.commandButton,
                                        toFill.x - outsets.left, toFill.y
                                        - outsets.top, toFill.width
                                        + outsets.left + outsets.right,
                                        2 * toFill.height, true);
                                g2d.setColor(borderColor);
                                g2d.drawLine(toFill.x + 1, toFill.y + toFill.height
                                        - 1, toFill.x + toFill.width - 2, toFill.y
                                        + toFill.height - 1);
                                break;
                            case LAST:
                                this.buttonRendererPane.paintComponent(g2d,
                                        this.rendererButton, this.commandButton,
                                        toFill.x - outsets.left, toFill.y
                                        - toFill.height, toFill.width
                                        + outsets.left + outsets.right, 2
                                        * toFill.height + outsets.bottom,
                                        true);
                                break;
                            case MIDDLE:
                                this.buttonRendererPane.paintComponent(g2d,
                                        this.rendererButton, this.commandButton,
                                        toFill.x - outsets.left, toFill.y
                                        - toFill.height, toFill.width
                                        + outsets.left + outsets.right,
                                        3 * toFill.height, true);
                                g2d.setColor(borderColor);
                                g2d.drawLine(toFill.x + 1, toFill.y + toFill.height
                                        - 1, toFill.x + toFill.width - 2, toFill.y
                                        + toFill.height - 1);
                        }
                    } else {
                        // horizontal
                        boolean ltr = this.commandButton
                                .getComponentOrientation().isLeftToRight();
                        if (locationKind == AbstractCommandButton.CommandButtonLocationOrderKind.MIDDLE) {
                            this.buttonRendererPane.paintComponent(g2d,
                                    this.rendererButton, this.commandButton,
                                    toFill.x - toFill.width, toFill.y
                                    - outsets.top, 3 * toFill.width,
                                    toFill.height + outsets.top
                                            + outsets.bottom, true);
                            g2d.setColor(borderColor);
                            g2d.drawLine(toFill.x + toFill.width - 1,
                                    toFill.y + 1, toFill.x + toFill.width - 1,
                                    toFill.y + toFill.height - 2);
                        } else {
                            boolean curveOnLeft = (ltr && (locationKind == AbstractCommandButton.CommandButtonLocationOrderKind.FIRST))
                                    || (!ltr && (locationKind == AbstractCommandButton.CommandButtonLocationOrderKind.LAST));
                            if (curveOnLeft) {
                                this.buttonRendererPane.paintComponent(g2d,
                                        this.rendererButton,
                                        this.commandButton, toFill.x
                                        - outsets.left, toFill.y
                                        - outsets.top,
                                        2 * toFill.width, toFill.height
                                        + outsets.top + outsets.bottom,
                                        true);
                                g2d.setColor(borderColor);
                                g2d.drawLine(toFill.x + toFill.width - 1,
                                        toFill.y + 1, toFill.x + toFill.width
                                        - 1, toFill.y + toFill.height
                                        - 2);
                            } else {
                                this.buttonRendererPane.paintComponent(g2d,
                                        this.rendererButton,
                                        this.commandButton, toFill.x
                                        - toFill.width, toFill.y
                                        - outsets.top, 2 * toFill.width
                                        + outsets.right, toFill.height
                                        + outsets.top + outsets.bottom,
                                        true);
                            }
                        }
                    }
                }
            } else {
                this.buttonRendererPane.paintComponent(g2d,
                        this.rendererButton, this.commandButton, toFill.x
                        - outsets.left, toFill.y - outsets.top,
                        toFill.width + outsets.left + outsets.right,
                        toFill.height + outsets.top + outsets.bottom, true);
            }
        }
        g2d.dispose();
    }

    /**
     * Updates the custom dimension.
     */
    protected void updateCustomDimension() {
        int dimension = this.commandButton.getCustomDimension();

        if (dimension > 0) {
            this.commandButton.getIcon().setDimension(
                    new Dimension(dimension, dimension));
            this.commandButton
                    .setDisplayState(CommandButtonDisplayState.FIT_TO_ICON);

            this.commandButton.invalidate();
            this.commandButton.revalidate();
            this.commandButton.doLayout();
            this.commandButton.repaint();
        }
    }

    /**
     * Updates the popup action icon.
     */
    protected void updatePopupActionIcon() {
        JCommandButton button = (JCommandButton) this.commandButton;
        if (button.getCommandButtonKind().hasPopup()) {
            this.popupActionIcon = this.createPopupActionIcon();
        } else {
            this.popupActionIcon = null;
        }
    }

    /**
     * Paints the button icon.
     *
     * @param g
     *            Graphics context.
     * @param iconRect
     *            Icon rectangle.
     */
    protected void paintButtonIcon(Graphics g, Rectangle iconRect) {
        Icon iconToPaint = this.getIconToPaint();
        if ((iconRect == null) || (iconToPaint == null)
                || (iconRect.width == 0) || (iconRect.height == 0)) {
            return;
        }

        iconToPaint.paintIcon(this.commandButton, g, iconRect.x, iconRect.y);
    }

    /*
      * (non-Javadoc)
      *
      * @see
      * javax.swing.plaf.ComponentUI#getPreferredSize(javax.swing.JComponent)
      */
    @Override
    public Dimension getPreferredSize(JComponent c) {
        AbstractCommandButton button = (AbstractCommandButton) c;
        return this.layoutManager.getPreferredSize(button);
    }

    @Override
    public CommandButtonLayoutInfo getLayoutInfo() {
        if (this.layoutInfo != null) {
            return this.layoutInfo;
        }
        this.layoutInfo = this.layoutManager.getLayoutInfo(commandButton,
                this.commandButton.getGraphics());
        return this.layoutInfo;
    }

    /**
     * Returns the layout gap for the visuals of the associated command button.
     *
     * @return The layout gap for the visuals of the associated command button.
     */
    protected int getLayoutGap() {
        Font font = this.commandButton.getFont();
        if (font == null)
            font = UIManager.getFont("Button.font");
        return (font.getSize() - 4) / 4;
    }

    /**
     * Returns indication whether the action-popup areas separator is painted.
     *
     * @return <code>true</code> if the action-popup areas separator is painted.
     */
    protected boolean isPaintingSeparators() {
        PopupButtonModel popupModel = (this.commandButton instanceof JCommandButton) ? ((JCommandButton) this.commandButton)
                .getPopupModel()
                : null;
        boolean isActionRollover = this.commandButton.getActionModel()
                .isRollover();
        boolean isPopupRollover = (popupModel != null)
                && popupModel.isRollover();
        // Rectangle actionArea = this.getActionClickArea();
        // Rectangle popupArea = this.getPopupClickArea();
        // boolean hasNonEmptyAreas = (actionArea.width * actionArea.height
        // * popupArea.width * popupArea.height > 0);
        return // hasNonEmptyAreas &&
                (isActionRollover || isPopupRollover);
    }

    /**
     * Returns indication whether the button background is painted.
     *
     * @return <code>true</code> if the button background is painted.
     */
    protected boolean isPaintingBackground() {
        PopupButtonModel popupModel = (this.commandButton instanceof JCommandButton) ? ((JCommandButton) this.commandButton)
                .getPopupModel()
                : null;
        boolean isActionSelected = this.commandButton.getActionModel()
                .isSelected();
        boolean isPopupSelected = (popupModel != null)
                && popupModel.isSelected();
        boolean isActionRollover = this.commandButton.getActionModel()
                .isRollover();
        boolean isPopupRollover = (popupModel != null)
                && popupModel.isRollover();
        boolean isPopupShowing = (popupModel != null)
                && (popupModel.isPopupShowing());
        boolean isActionArmed = this.commandButton.getActionModel().isArmed();
        boolean isPopupArmed = (popupModel != null) && (popupModel.isArmed());

        return (isActionSelected || isPopupSelected || isActionRollover
                || isPopupRollover || isPopupShowing || isActionArmed
                || isPopupArmed || !this.commandButton.isFlat());
    }

    /**
     * Creates the popup action listener for this command button.
     *
     * @return Popup action listener for this command button.
     */
    protected PopupActionListener createPopupActionListener() {
        return new PopupActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                processPopupAction();
            }
        };
    }

    protected void processPopupAction() {
        boolean wasPopupShowing = false;
        if (this.commandButton instanceof JCommandButton) {
            wasPopupShowing = ((JCommandButton) this.commandButton)
                    .getPopupModel().isPopupShowing();
        }

        // dismiss all the popups that are currently showing
        // up until <this> button.
        PopupPanelManager.defaultManager().hidePopups(commandButton);

        if (!(commandButton instanceof JCommandButton))
            return;

        if (wasPopupShowing)
            return;

        JCommandButton jcb = (JCommandButton) this.commandButton;

        // check if the command button has an associated popup
        // panel
        PopupPanelCallback popupCallback = jcb.getPopupCallback();
        final JPopupPanel popupPanel = (popupCallback != null) ? popupCallback
                .getPopupPanel(jcb) : null;
        if (popupPanel != null) {
            popupPanel.applyComponentOrientation(jcb.getComponentOrientation());
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    if ((commandButton == null) || (popupPanel == null))
                        return;

                    if (!commandButton.isShowing())
                        return;

                    popupPanel.doLayout();

                    int x = 0;
                    int y = 0;

                    JPopupPanel.PopupPanelCustomizer customizer = popupPanel
                            .getCustomizer();
                    boolean ltr = commandButton.getComponentOrientation()
                            .isLeftToRight();
                    Dimension popupPanelPrefSize = popupPanel.getPreferredSize();
                    if (customizer == null) {
                        switch (((JCommandButton) commandButton)
                                .getPopupOrientationKind()) {
                            case DOWNWARD:
                                if (ltr) {
                                    x = commandButton.getLocationOnScreen().x;
                                } else {
                                    x = commandButton.getLocationOnScreen().x
                                            + commandButton.getWidth()
                                            - popupPanelPrefSize.width;
                                }
                                y = commandButton.getLocationOnScreen().y
                                        + commandButton.getSize().height;
                                break;
                            case SIDEWARD:
                                if (ltr) {
                                    x = commandButton.getLocationOnScreen().x
                                            + commandButton.getWidth();
                                } else {
                                    x = commandButton.getLocationOnScreen().x
                                            - popupPanelPrefSize.width;
                                }
                                y = commandButton.getLocationOnScreen().y
                                        + getLayoutInfo().popupClickArea.y;
                                break;
                        }
                    } else {
                        Rectangle placementRect = customizer.getScreenBounds();
                        // System.out.println(placementRect);
                        x = placementRect.x;
                        y = placementRect.y;
                    }

                    // make sure that the popup stays in bounds
                    Rectangle scrBounds = commandButton
                            .getGraphicsConfiguration().getBounds();
                    int pw = popupPanelPrefSize.width;
                    int ph = popupPanelPrefSize.height;
                    if (pw > scrBounds.width || ph > scrBounds.height) {
                        pw = Math.min(pw, scrBounds.width);
                        ph = Math.min(ph, scrBounds.height);
                        popupPanelPrefSize = new Dimension(pw, ph);
                        popupPanel.setPreferredSize(popupPanelPrefSize);
                        popupPanel.setSize(popupPanelPrefSize);
                        popupPanel.doLayout();
                    }

                    if ((x + pw) > (scrBounds.x + scrBounds.width)) {
                        x = scrBounds.x + scrBounds.width - pw;
                    }
                    if ((y + ph) > (scrBounds.y + scrBounds.height)) {
                        y = scrBounds.y + scrBounds.height - ph;
                    }

                    // get the popup and show it
                    if (customizer != null) {
                        Rectangle placementRect = customizer.getScreenBounds();
                        popupPanel.setPreferredSize(new Dimension(
                                placementRect.width, placementRect.height));
                    }
                    Popup popup = PopupFactory.getSharedInstance().getPopup(
                            commandButton, popupPanel, x, y);
                    // System.out.println("Showing the popup panel");
                    PopupPanelManager.defaultManager().addPopup(commandButton,
                            popup, popupPanel);
                }
            });
        }
    }

    protected void syncDisabledIcon() {
        ResizableIcon currDisabledIcon = this.commandButton.getDisabledIcon();
        ResizableIcon icon = this.commandButton.getIcon();
        if ((currDisabledIcon == null)
                || (currDisabledIcon instanceof UIResource)) {
            if (icon != null) {
                this.commandButton.setDisabledIcon(new ResizableIconUIResource(
                        new FilteredResizableIcon(icon, new ColorConvertOp(
                                ColorSpace.getInstance(ColorSpace.CS_GRAY),
                                null))));
            } else {
                this.commandButton.setDisabledIcon(null);
            }
        } else {
            // disabled icon coming from app code
            if (icon != null) {
                this.commandButton.getDisabledIcon()
                        .setDimension(
                                new Dimension(icon.getIconWidth(), icon
                                        .getIconHeight()));
            }
        }
    }

    protected void syncIconDimension() {
        ResizableIcon icon = this.commandButton.getIcon();
        CommandButtonDisplayState commandButtonState = this.commandButton
                .getDisplayState();

        this.layoutManager = commandButtonState
                .createLayoutManager(this.commandButton);

        if (icon == null)
            return;

        int maxHeight = layoutManager.getPreferredIconSize();
        if (maxHeight < 0) {
            maxHeight = this.commandButton.getIcon().getIconHeight();
        }

        if (commandButtonState != CommandButtonDisplayState.FIT_TO_ICON) {
            @SuppressWarnings("SuspiciousNameCombination")
            Dimension newDim = new Dimension(maxHeight, maxHeight);
            icon.setDimension(newDim);
        }
    }

    /*
      * (non-Javadoc)
      *
      * @see
      * org.jvnet.flamingo.common.ui.CommandButtonUI#getKeyTipAnchorCenterPoint()
      */
    @Override
    public Point getKeyTipAnchorCenterPoint() {
        return this.layoutManager
                .getKeyTipAnchorCenterPoint(this.commandButton);
    }
}
