/*
 * Copyright (c) 2002, 2025, 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.sun.java.swing.plaf.gtk;

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;
import javax.swing.colorchooser.*;
import javax.swing.event.*;
import javax.swing.plaf.*;

/**
 * A color chooser panel mimicking that of GTK's: a color wheel showing
 * hue and a triangle that varies saturation and brightness.
 *
 * @author Scott Violet
 */
@SuppressWarnings("serial") // Superclass is not serializable across versions
class GTKColorChooserPanel extends AbstractColorChooserPanel implements
              ChangeListener {
    private static final float PI_3 = (float)(Math.PI / 3);

    private ColorTriangle triangle;
    private JLabel lastLabel;
    private JLabel label;

    private JSpinner hueSpinner;
    private JSpinner saturationSpinner;
    private JSpinner valueSpinner;

    private JSpinner redSpinner;
    private JSpinner greenSpinner;
    private JSpinner blueSpinner;

    private JTextField colorNameTF;

    private boolean settingColor;

    // The colors are mirrored to avoid creep in adjusting an individual
    // value.
    private float hue;
    private float saturation;
    private float brightness;



    /**
     * Convenience method to transfer focus to the next child of component.
     */
    // PENDING: remove this when a variant of this is added to awt.
    static void compositeRequestFocus(Component component, boolean direction) {
        if (component instanceof Container) {
            Container container = (Container)component;
            if (container.isFocusCycleRoot()) {
                FocusTraversalPolicy policy = container.
                                              getFocusTraversalPolicy();
                Component comp = policy.getDefaultComponent(container);
                if (comp!=null) {
                    comp.requestFocus();
                    return;
                }
            }
            Container rootAncestor = container.getFocusCycleRootAncestor();
            if (rootAncestor!=null) {
                FocusTraversalPolicy policy = rootAncestor.
                                                  getFocusTraversalPolicy();
                Component comp;

                if (direction) {
                    comp = policy.getComponentAfter(rootAncestor, container);
                }
                else {
                    comp = policy.getComponentBefore(rootAncestor, container);
                }
                if (comp != null) {
                    comp.requestFocus();
                    return;
                }
            }
        }
        component.requestFocus();
    }

    @Override
    public void setEnabled(boolean enabled) {
        super.setEnabled(enabled);
        setEnabled(this, enabled);
    }

    private static void setEnabled(Container container, boolean enabled) {
        for (Component component : container.getComponents()) {
            component.setEnabled(enabled);
            if (component instanceof Container) {
                setEnabled((Container) component, enabled);
            }
        }
    }

    /**
     * Returns a user presentable description of this GTKColorChooserPane.
     */
    @Override
    public String getDisplayName() {
        return (String)UIManager.get("GTKColorChooserPanel.nameText");
    }

    /**
     * Returns the mnemonic to use with <code>getDisplayName</code>.
     */
    @Override
    public int getMnemonic() {
        String m = (String)UIManager.get("GTKColorChooserPanel.mnemonic");

        if (m != null) {
            try {
                int value = Integer.parseInt(m);

                return value;
            } catch (NumberFormatException nfe) {}
        }
        return -1;
    }

    /**
     * Character to underline that represents the mnemonic.
     */
    @Override
    public int getDisplayedMnemonicIndex() {
        String m = (String)UIManager.get(
                           "GTKColorChooserPanel.displayedMnemonicIndex");

        if (m != null) {
            try {
                int value = Integer.parseInt(m);

                return value;
            } catch (NumberFormatException nfe) {}
        }
        return -1;
    }

    @Override
    public Icon getSmallDisplayIcon() {
        return null;
    }

    @Override
    public Icon getLargeDisplayIcon() {
        return null;
    }

    @Override
    public void uninstallChooserPanel(JColorChooser enclosingChooser) {
        super.uninstallChooserPanel(enclosingChooser);
        removeAll();
    }

    /**
     * Builds and configures the widgets for the GTKColorChooserPanel.
     */
    @Override
    protected void buildChooser() {
        triangle = new ColorTriangle();
        triangle.setName("GTKColorChooserPanel.triangle");

        // PENDING: when we straighten out user setting opacity, this should
        // be changed.
        label = new OpaqueLabel();
        label.setName("GTKColorChooserPanel.colorWell");
        label.setOpaque(true);
        label.setMinimumSize(new Dimension(67, 32));
        label.setPreferredSize(new Dimension(67, 32));
        label.setMaximumSize(new Dimension(67, 32));

        // PENDING: when we straighten out user setting opacity, this should
        // be changed.
        lastLabel = new OpaqueLabel();
        lastLabel.setName("GTKColorChooserPanel.lastColorWell");
        lastLabel.setOpaque(true);
        lastLabel.setMinimumSize(new Dimension(67, 32));
        lastLabel.setPreferredSize(new Dimension(67, 32));
        lastLabel.setMaximumSize(new Dimension(67, 32));

        hueSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 360, 1));
        configureSpinner(hueSpinner, "GTKColorChooserPanel.hueSpinner");
        saturationSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 255, 1));
        configureSpinner(saturationSpinner,
                         "GTKColorChooserPanel.saturationSpinner");
        valueSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 255, 1));
        configureSpinner(valueSpinner, "GTKColorChooserPanel.valueSpinner");
        redSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 255, 1));
        configureSpinner(redSpinner, "GTKColorChooserPanel.redSpinner");
        greenSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 255, 1));
        configureSpinner(greenSpinner, "GTKColorChooserPanel.greenSpinner");
        blueSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 255, 1));
        configureSpinner(blueSpinner, "GTKColorChooserPanel.blueSpinner");

        colorNameTF = new JTextField(8);

        setLayout(new GridBagLayout());

        add(this, "GTKColorChooserPanel.hue", hueSpinner, -1, -1);
        add(this, "GTKColorChooserPanel.red", redSpinner, -1, -1);
        add(this, "GTKColorChooserPanel.saturation", saturationSpinner, -1,-1);
        add(this, "GTKColorChooserPanel.green", greenSpinner, -1, -1);
        add(this, "GTKColorChooserPanel.value", valueSpinner, -1, -1);
        add(this, "GTKColorChooserPanel.blue", blueSpinner, -1, -1);

        add(new JSeparator(SwingConstants.HORIZONTAL), new
                  GridBagConstraints(1, 3, 4, 1, 1, 0,
                  GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL,
                  new Insets(14, 0, 0, 0), 0, 0));

        add(this, "GTKColorChooserPanel.colorName", colorNameTF, 0, 4);

        add(triangle, new GridBagConstraints(0, 0, 1, 5, 0, 0,
                      GridBagConstraints.LINE_START, GridBagConstraints.NONE,
                      new Insets(14, 20, 2, 9), 0, 0));

        Box hBox = Box.createHorizontalBox();
        hBox.add(lastLabel);
        hBox.add(label);
        add(hBox, new GridBagConstraints(0, 5, 1, 1, 0, 0,
                      GridBagConstraints.CENTER, GridBagConstraints.NONE,
                      new Insets(0, 0, 0, 0), 0, 0));

        add(new JSeparator(SwingConstants.HORIZONTAL), new
                  GridBagConstraints(0, 6, 5, 1, 1, 0,
                  GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL,
                  new Insets(12, 0, 0, 0), 0, 0));
    }

    /**
     * Configures the spinner.
     */
    private void configureSpinner(JSpinner spinner, String name) {
        spinner.addChangeListener(this);
        spinner.setName(name);
        JComponent editor = spinner.getEditor();
        if (editor instanceof JSpinner.DefaultEditor) {
            JFormattedTextField ftf = ((JSpinner.DefaultEditor)editor).
                                                 getTextField();

            ftf.setFocusLostBehavior(JFormattedTextField.COMMIT_OR_REVERT);
        }
    }

    /**
     * Adds the widget creating a JLabel with the specified name.
     */
    private void add(Container parent, String key, JComponent widget,
                     int x, int y) {
        JLabel label = new JLabel(UIManager.getString(key + "Text",
                                                      getLocale()));
        String mnemonic = (String)UIManager.get(key + "Mnemonic", getLocale());

        if (mnemonic != null) {
            try {
                label.setDisplayedMnemonic(Integer.parseInt(mnemonic));
            } catch (NumberFormatException nfe) {
            }
            String mnemonicIndex = (String)UIManager.get(key + "MnemonicIndex",
                                                    getLocale());

            if (mnemonicIndex != null) {
                try {
                    label.setDisplayedMnemonicIndex(Integer.parseInt(
                                                        mnemonicIndex));
                } catch (NumberFormatException nfe) {
                }
            }
        }
        label.setLabelFor(widget);
        if (x < 0) {
            x = parent.getComponentCount() % 4;
        }
        if (y < 0) {
            y = parent.getComponentCount() / 4;
        }
        GridBagConstraints con = new GridBagConstraints(x + 1, y, 1, 1, 0, 0,
                   GridBagConstraints.FIRST_LINE_END, GridBagConstraints.NONE,
                   new Insets(4, 0, 0, 4), 0, 0);
        if (y == 0) {
            con.insets.top = 14;
        }
        parent.add(label, con);
        con.gridx++;
        parent.add(widget, con);
    }

    /**
     * Refreshes the display from the model.
     */
    @Override
    public void updateChooser() {
        if (!settingColor) {
            lastLabel.setBackground(getColorFromModel());
            setColor(getColorFromModel(), true, true, false);
        }
    }

    /**
     * Resets the red component of the selected color.
     */
    private void setRed(int red) {
        setRGB(red << 16 | getColor().getGreen() << 8 | getColor().getBlue());
    }

    /**
     * Resets the green component of the selected color.
     */
    private void setGreen(int green) {
        setRGB(getColor().getRed() << 16 | green << 8 | getColor().getBlue());
    }

    /**
     * Resets the blue component of the selected color.
     */
    private void setBlue(int blue) {
        setRGB(getColor().getRed() << 16 | getColor().getGreen() << 8 | blue);
    }

    /**
     * Sets the hue of the selected color and updates the display if
     * necessary.
     */
    private void setHue(float hue, boolean update) {
        setHSB(hue, saturation, brightness);
        if (update) {
            settingColor = true;
            hueSpinner.setValue(Integer.valueOf((int)(hue * 360)));
            settingColor = false;
        }
    }

    /**
     * Returns the current amount of hue.
     */
    private float getHue() {
        return hue;
    }

    /**
     * Resets the saturation.
     */
    private void setSaturation(float saturation) {
        setHSB(hue, saturation, brightness);
    }

    /**
     * Returns the saturation.
     */
    private float getSaturation() {
        return saturation;
    }

    /**
     * Sets the brightness.
     */
    private void setBrightness(float brightness) {
        setHSB(hue, saturation, brightness);
    }

    /**
     * Returns the brightness.
     */
    private float getBrightness() {
        return brightness;
    }

    /**
     * Sets the saturation and brightness and updates the display if
     * necessary.
     */
    private void setSaturationAndBrightness(float s, float b, boolean update) {
        setHSB(hue, s, b);
        if (update) {
            settingColor = true;
            saturationSpinner.setValue(Integer.valueOf((int)(s * 255)));
            valueSpinner.setValue(Integer.valueOf((int)(b * 255)));
            settingColor = false;
        }
    }

    /**
     * Resets the rgb values.
     */
    private void setRGB(int rgb) {
        Color color = new Color(rgb);

        setColor(color, false, true, true);

        settingColor = true;
        hueSpinner.setValue(Integer.valueOf((int)(hue * 360)));
        saturationSpinner.setValue(Integer.valueOf((int)(saturation * 255)));
        valueSpinner.setValue(Integer.valueOf((int)(brightness * 255)));
        settingColor = false;
    }

    /**
     * Resets the hsb values.
     */
    private void setHSB(float h, float s, float b) {
        Color color = Color.getHSBColor(h, s, b);

        this.hue = h;
        this.saturation = s;
        this.brightness = b;
        setColor(color, false, false, true);

        settingColor = true;
        redSpinner.setValue(Integer.valueOf(color.getRed()));
        greenSpinner.setValue(Integer.valueOf(color.getGreen()));
        blueSpinner.setValue(Integer.valueOf(color.getBlue()));
        settingColor = false;
    }


    /**
     * Rests the color.
     *
     * @param color new Color
     * @param updateSpinners whether or not to update the spinners.
     * @param updateHSB if true, the hsb fields are updated based on the
     *                  new color
     * @param updateModel if true, the model is set.
     */
    private void setColor(Color color, boolean updateSpinners,
                          boolean updateHSB, boolean updateModel) {
        if (color == null) {
            color = Color.BLACK;
        }

        settingColor = true;

        if (updateHSB) {
            float[] hsb = Color.RGBtoHSB(color.getRed(), color.getGreen(),
                                         color.getBlue(), null);
            hue = hsb[0];
            saturation = hsb[1];
            brightness = hsb[2];
        }

        if (updateModel) {
            ColorSelectionModel model = getColorSelectionModel();
            if (model != null) {
                model.setSelectedColor(color);
            }
        }

        triangle.setColor(hue, saturation, brightness);
        label.setBackground(color);
        // Force Integer to pad the string with 0's by adding 0x1000000 and
        // then removing the first character.
        String hexString = Integer.toHexString(
                  (color.getRGB() & 0xFFFFFF) | 0x1000000);
        colorNameTF.setText("#" + hexString.substring(1));

        if (updateSpinners) {
            redSpinner.setValue(Integer.valueOf(color.getRed()));
            greenSpinner.setValue(Integer.valueOf(color.getGreen()));
            blueSpinner.setValue(Integer.valueOf(color.getBlue()));

            hueSpinner.setValue(Integer.valueOf((int)(hue * 360)));
            saturationSpinner.setValue(Integer.valueOf((int)(saturation * 255)));
            valueSpinner.setValue(Integer.valueOf((int)(brightness * 255)));
        }
        settingColor = false;
    }

    public Color getColor() {
        return label.getBackground();
    }

    /**
     * ChangeListener method, updates the necessary display widgets.
     */
    @Override
    public void stateChanged(ChangeEvent e) {
        if (settingColor) {
            return;
        }
        Color color = getColor();

        if (e.getSource() == hueSpinner) {
            setHue(((Number)hueSpinner.getValue()).floatValue() / 360, false);
        }
        else if (e.getSource() == saturationSpinner) {
            setSaturation(((Number)saturationSpinner.getValue()).
                          floatValue() / 255);
        }
        else if (e.getSource() == valueSpinner) {
            setBrightness(((Number)valueSpinner.getValue()).
                          floatValue() / 255);
        }
        else if (e.getSource() == redSpinner) {
            setRed(((Number)redSpinner.getValue()).intValue());
        }
        else if (e.getSource() == greenSpinner) {
            setGreen(((Number)greenSpinner.getValue()).intValue());
        }
        else if (e.getSource() == blueSpinner) {
            setBlue(((Number)blueSpinner.getValue()).intValue());
        }
    }



    /**
     * Flag indicating the angle, or hue, has changed and the triangle
     * needs to be recreated.
     */
    private static final int FLAGS_CHANGED_ANGLE = 1 << 0;
    /**
     * Indicates the wheel is being dragged.
     */
    private static final int FLAGS_DRAGGING = 1 << 1;
    /**
     * Indicates the triangle is being dragged.
     */
    private static final int FLAGS_DRAGGING_TRIANGLE = 1 << 2;
    /**
     * Indicates a color is being set and we should ignore setColor
     */
    private static final int FLAGS_SETTING_COLOR = 1 << 3;
    /**
     * Indicates the wheel has focus.
     */
    private static final int FLAGS_FOCUSED_WHEEL = 1 << 4;
    /**
     * Indicates the triangle has focus.
     */
    private static final int FLAGS_FOCUSED_TRIANGLE = 1 << 5;


    /**
     * Class responsible for rendering a color wheel and color triangle.
     */
    @SuppressWarnings("serial") // Superclass is not serializable across versions
    private class ColorTriangle extends JPanel {
        /**
         * Cached image of the wheel.
         */
        private Image wheelImage;

        /**
         * Cached image of the triangle.
         */
        private Image triangleImage;

        /**
         * Angle triangle is rotated by.
         */
        private double angle;

        /**
         * Boolean bitmask.
         */
        private int flags;

        /**
         * X location of selected color indicator.
         */
        private int circleX;
        /**
         * Y location of selected color indicator.
         */
        private int circleY;


        public ColorTriangle() {
            enableEvents(AWTEvent.FOCUS_EVENT_MASK);
            enableEvents(AWTEvent.MOUSE_EVENT_MASK);
            enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK);

            setMinimumSize(new Dimension(getWheelRadius() * 2 + 2,
                                         getWheelRadius() * 2 + 2));
            setPreferredSize(new Dimension(getWheelRadius() * 2 + 2,
                                           getWheelRadius() * 2 + 2));

            // We want to handle tab ourself.
            setFocusTraversalKeysEnabled(false);

            // PENDING: this should come from the style.
            getInputMap().put(KeyStroke.getKeyStroke("UP"), "up");
            getInputMap().put(KeyStroke.getKeyStroke("DOWN"), "down");
            getInputMap().put(KeyStroke.getKeyStroke("LEFT"), "left");
            getInputMap().put(KeyStroke.getKeyStroke("RIGHT"), "right");

            getInputMap().put(KeyStroke.getKeyStroke("KP_UP"), "up");
            getInputMap().put(KeyStroke.getKeyStroke("KP_DOWN"), "down");
            getInputMap().put(KeyStroke.getKeyStroke("KP_LEFT"), "left");
            getInputMap().put(KeyStroke.getKeyStroke("KP_RIGHT"), "right");

            getInputMap().put(KeyStroke.getKeyStroke("TAB"), "focusNext");
            getInputMap().put(KeyStroke.getKeyStroke("shift TAB"),"focusLast");

            ActionMap map = (ActionMap)UIManager.get(
                                       "GTKColorChooserPanel.actionMap");

            if (map == null) {
                map = new ActionMapUIResource();
                map.put("left", new ColorAction("left", 2));
                map.put("right", new ColorAction("right", 3));
                map.put("up", new ColorAction("up", 0));
                map.put("down", new ColorAction("down", 1));
                map.put("focusNext", new ColorAction("focusNext", 4));
                map.put("focusLast", new ColorAction("focusLast", 5));
                UIManager.getLookAndFeelDefaults().put(
                             "GTKColorChooserPanel.actionMap", map);
            }
            SwingUtilities.replaceUIActionMap(this, map);
        }

        /**
         * Returns the GTKColorChooserPanel.
         */
        GTKColorChooserPanel getGTKColorChooserPanel() {
            return GTKColorChooserPanel.this;
        }

        /**
         * Gives focus to the wheel.
         */
        void focusWheel() {
            setFocusType(1);
        }

        /**
         * Gives focus to the triangle.
         */
        void focusTriangle() {
            setFocusType(2);
        }

        /**
         * Returns true if the wheel currently has focus.
         */
        boolean isWheelFocused() {
            return isSet(FLAGS_FOCUSED_WHEEL);
        }

        /**
         * Resets the selected color.
         */
        public void setColor(float h, float s, float b) {
            if (isSet(FLAGS_SETTING_COLOR)) {
                return;
            }

            setAngleFromHue(h);
            setSaturationAndBrightness(s, b);
        }

        /**
         * Returns the selected color.
         */
        public Color getColor() {
            return GTKColorChooserPanel.this.getColor();
        }

        /**
         * Returns the x location of the selected color indicator.
         */
        int getColorX() {
            return circleX + getIndicatorSize() / 2 - getWheelXOrigin();
        }

        /**
         * Returns the y location of the selected color indicator.
         */
        int getColorY() {
            return circleY + getIndicatorSize() / 2 - getWheelYOrigin();
        }

        @Override
        protected void processEvent(AWTEvent e) {

            if (!(getGTKColorChooserPanel().isEnabled())) {
                return;
            }

            if (e.getID() == MouseEvent.MOUSE_PRESSED ||
                   ((isSet(FLAGS_DRAGGING) ||isSet(FLAGS_DRAGGING_TRIANGLE)) &&
                   e.getID() == MouseEvent.MOUSE_DRAGGED)) {
                // Assign focus to either the wheel or triangle and attempt
                // to drag either the wheel or triangle.
                int size = getWheelRadius();
                int x = ((MouseEvent)e).getX() - size;
                int y = ((MouseEvent)e).getY() - size;

                if (!hasFocus()) {
                    requestFocus(FocusEvent.Cause.MOUSE_EVENT);
                }
                if (!isSet(FLAGS_DRAGGING_TRIANGLE) &&
                      adjustHue(x, y, e.getID() == MouseEvent.MOUSE_PRESSED)) {
                    setFlag(FLAGS_DRAGGING, true);
                    setFocusType(1);
                }
                else if (adjustSB(x, y, e.getID() ==
                                        MouseEvent.MOUSE_PRESSED)) {
                    setFlag(FLAGS_DRAGGING_TRIANGLE, true);
                    setFocusType(2);
                }
                else {
                    setFocusType(2);
                }
            }
            else if (e.getID() == MouseEvent.MOUSE_RELEASED) {
                // Stopped dragging
                setFlag(FLAGS_DRAGGING_TRIANGLE, false);
                setFlag(FLAGS_DRAGGING, false);
            }
            else if (e.getID() == FocusEvent.FOCUS_LOST) {
                // Reset the flags to indicate no one has focus
                setFocusType(0);
            }
            else if (e.getID() == FocusEvent.FOCUS_GAINED) {
                // Gained focus, reassign focus to the wheel if no one
                // currently has focus.
                if (!isSet(FLAGS_FOCUSED_TRIANGLE) &&
                          !isSet(FLAGS_FOCUSED_WHEEL)) {
                    setFlag(FLAGS_FOCUSED_WHEEL, true);
                    setFocusType(1);
                }
                repaint();
            }
            super.processEvent(e);
        }

        @Override
        public void paintComponent(Graphics g) {
            super.paintComponent(g);

            // Draw the wheel and triangle
            int size = getWheelRadius();
            int width = getWheelWidth();
            Image image = getImage(size);
            g.drawImage(image, getWheelXOrigin() - size,
                        getWheelYOrigin() - size, null);

            // Draw the focus indicator for the wheel
            if (hasFocus() && isSet(FLAGS_FOCUSED_WHEEL)) {
                g.setColor(Color.BLACK);
                g.drawOval(getWheelXOrigin() - size, getWheelYOrigin() - size,
                           2 * size, 2 * size);
                g.drawOval(getWheelXOrigin() - size + width, getWheelYOrigin()-
                           size + width, 2 * (size - width), 2 *
                           (size - width));
            }

            // Draw a line on the wheel indicating the selected hue.
            if (Math.toDegrees(Math.PI * 2 - angle) <= 20 ||
                     Math.toDegrees(Math.PI * 2 - angle) >= 201) {
                g.setColor(Color.WHITE);
            }
            else {
                g.setColor(Color.BLACK);
            }
            int lineX0 = (int)(Math.cos(angle) * size);
            int lineY0 = (int)(Math.sin(angle) * size);
            int lineX1 = (int)(Math.cos(angle) * (size - width));
            int lineY1 = (int)(Math.sin(angle) * (size - width));
            g.drawLine(lineX0 + size, lineY0 + size, lineX1 + size,
                       lineY1 + size);

            // Draw the focus indicator on the triangle
            if (hasFocus() && isSet(FLAGS_FOCUSED_TRIANGLE)) {
                Graphics g2 = g.create();
                int innerR = getTriangleCircumscribedRadius();
                int a = (int)(3 * innerR / Math.sqrt(3));
                g2.translate(getWheelXOrigin(), getWheelYOrigin());
                ((Graphics2D)g2).rotate(angle + Math.PI / 2);
                g2.setColor(Color.BLACK);
                g2.drawLine(0, -innerR, a / 2, innerR / 2);
                g2.drawLine(a / 2, innerR / 2, -a / 2, innerR / 2);
                g2.drawLine(-a / 2, innerR / 2, 0, -innerR);
                g2.dispose();
            }

            // Draw the selected color indicator.
            g.setColor(Color.BLACK);
            g.drawOval(circleX, circleY, getIndicatorSize() - 1,
                       getIndicatorSize() - 1);
            g.setColor(Color.WHITE);
            g.drawOval(circleX + 1, circleY + 1, getIndicatorSize() - 3,
                       getIndicatorSize() - 3);
        }

        /**
         * Returns an image representing the triangle and wheel.
         */
        private Image getImage(int size) {
            if (!isSet(FLAGS_CHANGED_ANGLE) && wheelImage != null &&
                        wheelImage.getWidth(null) == size * 2) {
                return wheelImage;
            }
            if (wheelImage == null || wheelImage.getWidth(null) != size) {
                wheelImage = getWheelImage(size);
            }
            int innerR = getTriangleCircumscribedRadius();
            int triangleSize = (int)(innerR * 3.0 / 2.0);
            int a = (int)(2 * triangleSize / Math.sqrt(3));
            if (triangleImage == null || triangleImage.getWidth(null) != a) {
                triangleImage = new BufferedImage(a, a,
                                                  BufferedImage.TYPE_INT_ARGB);
            }
            Graphics g = triangleImage.getGraphics();
            g.setColor(new Color(0, 0, 0, 0));
            g.fillRect(0, 0, a, a);
            g.translate(a / 2, 0);
            paintTriangle(g, triangleSize, getColor());
            g.translate(-a / 2, 0);
            g.dispose();

            g = wheelImage.getGraphics();
            g.setColor(new Color(0, 0, 0, 0));
            g.fillOval(getWheelWidth(), getWheelWidth(),
                       2 * (size - getWheelWidth()),
                       2 * (size - getWheelWidth()));

            double rotate = Math.toRadians(-30.0) + angle;
            g.translate(size, size);
            ((Graphics2D)g).rotate(rotate);
            g.drawImage(triangleImage, -a / 2,
                        getWheelWidth() - size, null);
            ((Graphics2D)g).rotate(-rotate);
            g.translate(a / 2, size - getWheelWidth());

            setFlag(FLAGS_CHANGED_ANGLE, false);

            return wheelImage;
        }

        private void paintTriangle(Graphics g, int size, Color color) {
            float[] colors = Color.RGBtoHSB(color.getRed(),
                                            color.getGreen(),
                                            color.getBlue(), null);
            float hue = colors[0];
            double dSize = (double)size;
            for (int y = 0; y < size; y++) {
                int maxX = (int)(y * Math.tan(Math.toRadians(30.0)));
                float factor = maxX * 2;
                if (maxX > 0) {
                    float value = (float)(y / dSize);
                    for (int x = -maxX; x <= maxX; x++) {
                        float saturation = (float)x / factor + .5f;
                        g.setColor(Color.getHSBColor(hue, saturation, value));
                        g.fillRect(x, y, 1, 1);
                    }
                }
                else {
                    g.setColor(color);
                    g.fillRect(0, y, 1, 1);
                }
            }
        }

        /**
         * Returns a color wheel image for the specified size.
         *
         * @param size Integer giving size of color wheel.
         * @return Color wheel image
         */
        private Image getWheelImage(int size) {
            int minSize = size - getWheelWidth();
            int doubleSize = size * 2;
            BufferedImage image = new BufferedImage(doubleSize, doubleSize,
                                              BufferedImage.TYPE_INT_ARGB);

            for (int y = -size; y < size; y++) {
                int ySquared = y * y;
                for (int x = -size; x < size; x++) {
                    double rad = Math.sqrt(ySquared + x * x);

                    if (rad < size && rad > minSize) {
                        int rgb = colorWheelLocationToRGB(x, y, rad) |
                              0xFF000000;
                        image.setRGB(x + size, y + size, rgb);
                    }
                }
            }
            wheelImage = image;
            return wheelImage;
        }

        /**
         * Adjusts the saturation and brightness. <code>x</code> and
         * <code>y</code> give the location to adjust to and are relative
         * to the origin of the wheel/triangle.
         *
         * @param x X coordinate on the triangle to adjust to
         * @param y Y coordinate on the triangle to adjust to
         * @param checkLoc if true the location is checked to make sure
         *        it is contained in the triangle, if false the location is
         *        constrained to fit in the triangle.
         * @return true if the location is valid
         */
        boolean adjustSB(int x, int y, boolean checkLoc) {
            int innerR = getWheelRadius() - getWheelWidth();
            boolean resetXY = false;
            // Invert the axis.
            y = -y;
            if (checkLoc && (x < -innerR || x > innerR || y < -innerR ||
                             y > innerR)) {
                return false;
            }
            // Rotate to origin and verify x is valid.
            int triangleSize = innerR * 3 / 2;
            double x1 = Math.cos(angle) * x - Math.sin(angle) * y;
            double y1 = Math.sin(angle) * x + Math.cos(angle) * y;
            if (x1 < -(innerR / 2)) {
                if (checkLoc) {
                    return false;
                }
                x1 = -innerR / 2;
                resetXY = true;
            }
            else if ((int)x1 > innerR) {
                if (checkLoc) {
                    return false;
                }
                x1 = innerR;
                resetXY = true;
            }
            // Verify y location is valid.
            int maxY = (int)((triangleSize - x1 - innerR / 2.0) *
                             Math.tan(Math.toRadians(30.0)));
            if (y1 <= -maxY) {
                if (checkLoc) {
                    return false;
                }
                y1 = -maxY;
                resetXY = true;
            }
            else if (y1 > maxY) {
                if (checkLoc) {
                    return false;
                }
                y1 = maxY;
                resetXY = true;
            }
            // Rotate again to determine value and scale
            double x2 = Math.cos(Math.toRadians(-30.0)) * x1 -
                 Math.sin(Math.toRadians(-30.0)) * y1;
            double y2 = Math.sin(Math.toRadians(-30.0)) * x1 +
                 Math.cos(Math.toRadians(-30.0)) * y1;
            float value = Math.min(1.0f, (float)((innerR - y2) /
                                                (double)triangleSize));
            float maxX = (float)(Math.tan(Math.toRadians(30)) * (innerR - y2));
            float saturation = Math.min(1.0f, (float)(x2 / maxX / 2 + .5));

            setFlag(FLAGS_SETTING_COLOR, true);
            if (resetXY) {
                setSaturationAndBrightness(saturation, value);
            }
            else {
                setSaturationAndBrightness(saturation, value, x +
                                      getWheelXOrigin(),getWheelYOrigin() - y);
            }
            GTKColorChooserPanel.this.setSaturationAndBrightness(saturation,
                                                                 value, true);
            setFlag(FLAGS_SETTING_COLOR, false);
            return true;
        }

        /**
         * Sets the saturation and brightness.
         */
        private void setSaturationAndBrightness(float s, float b) {
            int innerR = getTriangleCircumscribedRadius();
            int triangleSize = innerR * 3 / 2;
            double x = b * triangleSize;
            double maxY = x * Math.tan(Math.toRadians(30.0));
            double y = 2 * maxY * s - maxY;
            x = x - innerR;
            double x1 = Math.cos(Math.toRadians(-60.0) - angle) *
                        x - Math.sin(Math.toRadians(-60.0) - angle) * y;
            double y1 = Math.sin(Math.toRadians(-60.0) - angle) * x +
                        Math.cos(Math.toRadians(-60.0) - angle) * y;
            int newCircleX = (int)x1 + getWheelXOrigin();
            int newCircleY = getWheelYOrigin() - (int)y1;

            setSaturationAndBrightness(s, b, newCircleX, newCircleY);
        }


        /**
         * Sets the saturation and brightness.
         */
        private void setSaturationAndBrightness(float s, float b,
                                             int newCircleX, int newCircleY) {
            newCircleX -= getIndicatorSize() / 2;
            newCircleY -= getIndicatorSize() / 2;

            int minX = Math.min(newCircleX, circleX);
            int minY = Math.min(newCircleY, circleY);

            repaint(minX, minY, Math.max(circleX, newCircleX) - minX +
                    getIndicatorSize() + 1, Math.max(circleY, newCircleY) -
                    minY + getIndicatorSize() + 1);
            circleX = newCircleX;
            circleY = newCircleY;
        }

        /**
         * Adjusts the hue based on the passed in location.
         *
         * @param x X location to adjust to, relative to the origin of the
         *        wheel
         * @param y Y location to adjust to, relative to the origin of the
         *        wheel
         * @param check if true the location is checked to make sure
         *        it is contained in the wheel, if false the location is
         *        constrained to fit in the wheel
         * @return true if the location is valid.
         */
        private boolean adjustHue(int x, int y, boolean check) {
            double rad = Math.sqrt(x * x + y * y);
            int size = getWheelRadius();

            if (!check || (rad >= size - getWheelWidth() && rad < size)) {
                // Map the location to an angle and reset hue
                double angle;
                if (x == 0) {
                    if (y > 0) {
                        angle = Math.PI / 2.0;
                    }
                    else {
                        angle = Math.PI + Math.PI / 2.0;
                    }
                }
                else {
                    angle = Math.atan((double)y / (double)x);
                    if (x < 0) {
                        angle += Math.PI;
                    }
                    else if (angle < 0) {
                        angle += 2 * Math.PI;
                    }
                }
                setFlag(FLAGS_SETTING_COLOR, true);
                setHue((float)(1.0 - angle / Math.PI / 2), true);
                setFlag(FLAGS_SETTING_COLOR, false);
                setHueAngle(angle);
                setSaturationAndBrightness(getSaturation(), getBrightness());
                return true;
            }
            return false;
        }

        /**
         * Rotates the triangle to accommodate the passed in hue.
         */
        private void setAngleFromHue(float hue) {
            setHueAngle((1.0 - hue) * Math.PI * 2);
        }

        /**
         * Sets the angle representing the hue.
         */
        private void setHueAngle(double angle) {
            double oldAngle = this.angle;

            this.angle = angle;
            if (angle != oldAngle) {
                setFlag(FLAGS_CHANGED_ANGLE, true);
                repaint();
            }
        }

        /**
         * Returns the size of the color indicator.
         */
        private int getIndicatorSize() {
            return 8;
        }

        /**
         * Returns the circumscribed radius of the triangle.
         */
        private int getTriangleCircumscribedRadius() {
            return 72;
        }

        /**
         * Returns the x origin of the wheel and triangle.
         */
        private int getWheelXOrigin() {
            return 85;
        }

        /**
         * Returns y origin of the wheel and triangle.
         */
        private int getWheelYOrigin() {
            return 85;
        }

        /**
         * Returns the width of the wheel.
         */
        private int getWheelWidth() {
            return 13;
        }

        /**
         * Sets the focus to one of: 0 no one, 1 the wheel or 2 the triangle.
         */
        private void setFocusType(int type) {
            if (type == 0) {
                setFlag(FLAGS_FOCUSED_WHEEL, false);
                setFlag(FLAGS_FOCUSED_TRIANGLE, false);
                repaint();
            }
            else {
                int toSet = FLAGS_FOCUSED_WHEEL;
                int toUnset = FLAGS_FOCUSED_TRIANGLE;

                if (type == 2) {
                    toSet = FLAGS_FOCUSED_TRIANGLE;
                    toUnset = FLAGS_FOCUSED_WHEEL;
                }
                if (!isSet(toSet)) {
                    setFlag(toSet, true);
                    repaint();
                    setFlag(toUnset, false);
                }
            }
        }

        /**
         * Returns the radius of the wheel.
         */
        private int getWheelRadius() {
            // As far as I can tell, GTK doesn't allow stretching this
            // widget
            return 85;
        }

        /**
         * Updates the flags bitmask.
         */
        private void setFlag(int flag, boolean value) {
            if (value) {
                flags |= flag;
            }
            else {
                flags &= ~flag;
            }
        }

        /**
         * Returns true if a particular flag has been set.
         */
        private boolean isSet(int flag) {
            return ((flags & flag) == flag);
        }

        /**
         * Returns the RGB color to use for the specified location. The
         * passed in point must be on the color wheel and be relative to the
         * origin of the color wheel.
         *
         * @param x X location to get color for
         * @param y Y location to get color for
         * @param rad Radius from center of color wheel
         * @return integer with red, green and blue components
         */
        private int colorWheelLocationToRGB(int x, int y, double rad) {
            double angle = Math.acos((double)x / rad);
            int rgb;

            if (angle < PI_3) {
                if (y < 0) {
                    // FFFF00 - FF0000
                    rgb = 0xFF0000 | Math.min(255,
                                           (int)(255 * angle / PI_3)) << 8;
                }
                else {
                    // FF0000 - FF00FF
                    rgb = 0xFF0000 | Math.min(255,
                                           (int)(255 * angle / PI_3));
                }
            }
            else if (angle < 2 * PI_3) {
                angle -= PI_3;
                if (y < 0) {
                    // 00FF00 - FFFF00
                    rgb = 0x00FF00 | Math.max(0, 255 -
                                           (int)(255 * angle / PI_3)) << 16;
                }
                else {
                    // FF00FF - 0000FF
                    rgb = 0x0000FF | Math.max(0, 255 -
                                           (int)(255 * angle / PI_3)) << 16;
                }
            }
            else {
                angle -= 2 * PI_3;
                if (y < 0) {
                    // 00FFFF - 00FF00
                    rgb = 0x00FF00 | Math.min(255,
                                           (int)(255 * angle / PI_3));
                }
                else {
                    // 0000FF - 00FFFF
                    rgb = 0x0000FF | Math.min(255,
                                           (int)(255 * angle / PI_3)) << 8;
                }
            }
            return rgb;
        }

        /**
         * Increments the hue.
         */
        void incrementHue(boolean positive) {
            float hue = triangle.getGTKColorChooserPanel().getHue();

            if (positive) {
                hue += 1.0f / 360.0f;
            }
            else {
                hue -= 1.0f / 360.0f;
            }
            if (hue > 1) {
                hue -= 1;
            }
            else if (hue < 0) {
                hue += 1;
            }
            getGTKColorChooserPanel().setHue(hue, true);
        }
    }


    /**
     * Action class used for colors.
     */
    @SuppressWarnings("serial") // Superclass is not serializable across versions
    private static class ColorAction extends AbstractAction {
        private int type;

        ColorAction(String name, int type) {
            super(name);
            this.type = type;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            ColorTriangle triangle = (ColorTriangle)e.getSource();

            if (triangle.isWheelFocused()) {
                float hue = triangle.getGTKColorChooserPanel().getHue();

                switch (type) {
                case 0:
                case 2:
                    triangle.incrementHue(true);
                    break;
                case 1:
                case 3:
                    triangle.incrementHue(false);
                    break;
                case 4:
                    triangle.focusTriangle();
                    break;
                case 5:
                    compositeRequestFocus(triangle, false);
                    break;
                }
            }
            else {
                int xDelta = 0;
                int yDelta = 0;

                switch (type) {
                case 0:
                    // up
                    yDelta--;
                    break;
                case 1:
                    // down
                    yDelta++;
                    break;
                case 2:
                    // left
                    xDelta--;
                    break;
                case 3:
                    // right
                    xDelta++;
                    break;
                case 4:
                    compositeRequestFocus(triangle, true);
                    return;
                case 5:
                    triangle.focusWheel();
                    return;
                }
                triangle.adjustSB(triangle.getColorX() + xDelta,
                                  triangle.getColorY() + yDelta, true);
            }
        }
    }

    @SuppressWarnings("serial") // Superclass is not serializable across versions
    private static class OpaqueLabel extends JLabel {
        @Override
        public boolean isOpaque() {
            return true;
        }
    }
}
