/*
 * NullableCheckBox.java 7 nov. 2008
 *
 * Sweet Home 3D, Copyright (c) 2008 Emmanuel PUYBARET / eTeks <info@eteks.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package com.eteks.sweethome3d.swing;

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

/**
 * A check box that accepts <code>null</code> values. Thus this check box is able to
 * display 3 states : <code>null</code>, <code>false</code> and <code>true</code>.
 * @author Emmanuel Puybaret
 */
public class NullableCheckBox extends JComponent {    
  /** 
   * Identifies a change in the check box text. 
   */
  public static final String TEXT_CHANGED_PROPERTY = "text";
  /** 
   * Identifies a change in the check box mnemonic. 
   */
  public static final String MNEMONIC_CHANGED_PROPERTY = "mnemonic";
  
  private JCheckBox    checkBox;
  private Boolean      value = Boolean.FALSE;
  private boolean      nullable;
  private List<ChangeListener> changeListeners = new ArrayList<ChangeListener>(1);
  
  /**
   * Creates a nullable check box.
   */
  public NullableCheckBox(String text) {
    // Measure check box size alone without its text
    final Dimension checkBoxSize = new JCheckBox().getPreferredSize();
    // Create a check box that displays a dash upon default check box for a null value
    this.checkBox = new JCheckBox(text) {
      @Override
      protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        if (value == null) {
          g.drawRect(checkBoxSize.width / 2 - 3, checkBoxSize.height / 2, 6, 1);
        }
      }
    };
    // Add an item listener to change default checking logic 
    ItemListener checkBoxListener = new ItemListener() {
      public void itemStateChanged(ItemEvent ev) {
        ev.getItemSelectable().removeItemListener(this);
        // If this check box is nullable
        if (nullable) {
          // Checking sequence will be null, true, false
          if (getValue() == Boolean.FALSE) {
            setValue(null);
          } else if (getValue() == null) {
            setValue(Boolean.TRUE);
          } else {
            setValue(Boolean.FALSE);
          }
        } else {
          setValue(checkBox.isSelected());
        }
        ev.getItemSelectable().addItemListener(this);
      }
    };
    this.checkBox.addItemListener(checkBoxListener);
    
    // Add the check box and its label to this component
    setLayout(new GridLayout());
    add(this.checkBox);
  }
  
  /**
   * Returns <code>null</code>, <code>Boolean.TRUE</code> or <code>Boolean.FALSE</code>.
   */
  public Boolean getValue() {
    return this.value;
  }

  /**
   * Sets displayed value in check box. 
   * @param value <code>null</code>, <code>Boolean.TRUE</code> or <code>Boolean.FALSE</code>
   */
  public void setValue(Boolean value) {
    if (value != this.value) {
      this.value = value;
      if (value != null) {
        this.checkBox.setSelected(value);
      } else if (isNullable()) {
        // Unselect check box to display a dash in its middle
        this.checkBox.setSelected(false);
      } else {
        throw new IllegalArgumentException("Check box isn't nullable");
      }
      this.checkBox.repaint();
      fireStateChanged();
    }
  }
  
  /**
   * Returns <code>true</code> if this check box is nullable.
   */
  public boolean isNullable() {
    return this.nullable;
  }

  /**
   * Sets whether this check box is nullable.
   */
  public void setNullable(boolean nullable) {
    this.nullable = nullable;
    if (!nullable && getValue() == null) {
      setValue(Boolean.FALSE);
    }
  }
  
  /**
   * Sets the mnemonic of this component.
   * @param mnemonic a <code>VK_...</code> code defined in <code>java.awt.event.KeyEvent</code>. 
   */
  public void setMnemonic(int mnemonic) {
    int oldMnemonic = this.checkBox.getMnemonic();
    if (oldMnemonic != mnemonic) {
      this.checkBox.setMnemonic(mnemonic);
      firePropertyChange(MNEMONIC_CHANGED_PROPERTY, oldMnemonic, mnemonic);
    }
  }
  
  /**
   * Returns the mnemonic of this component.
   */
  public int getMnemonic() {
    return this.checkBox.getMnemonic();
  }

  /**
   * Sets the text of this component.
   * @param text a <code>VK_...</code> code defined in <code>java.awt.event.KeyEvent</code>. 
   */
  public void setText(String text) {
    String oldText = this.checkBox.getText();
    if (oldText != text) {
      this.checkBox.setText(text);
      firePropertyChange(TEXT_CHANGED_PROPERTY, oldText, text);
    }
  }
  
  /**
   * Returns the text of this component.
   */
  public String getText() {
    return this.checkBox.getText();
  }
  
  /**
   * Sets the tool tip text displayed by this check box.
   */
  public void setToolTipText(String text) {
    this.checkBox.setToolTipText(text);
  }

  @Override
  public void setEnabled(boolean enabled) {
    if (this.checkBox.isEnabled() != enabled) {
      this.checkBox.setEnabled(enabled);
      firePropertyChange("enabled", !enabled, enabled);
    }
  }

  @Override
  public boolean isEnabled() {
    return this.checkBox.isEnabled();
  }
  
  /**
   * Adds a listener to this component.
   */
  public void addChangeListener(final ChangeListener l) {
    this.changeListeners.add(l);
  }

  /**
   * Removes a listener from this component.
   */
  public void removeChangeListener(final ChangeListener l) {
    this.changeListeners.remove(l);
  }

  /**
   * Fires a state changed event to listeners.
   */
  private void fireStateChanged() {
    if (!this.changeListeners.isEmpty()) {
      ChangeEvent changeEvent = new ChangeEvent(this);
      // Work on a copy of changeListeners to ensure a listener 
      // can modify safely listeners list
      ChangeListener [] listeners = this.changeListeners.
        toArray(new ChangeListener [this.changeListeners.size()]);
      for (ChangeListener listener : listeners) {
        listener.stateChanged(changeEvent);
      }
    }
  }
}