import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;

/** DemoComboBox2 - similar to <code>DemoComboBox</code> except
* with a few additional features.<p>
*
* <b>Editable Combobox</b><br>
* The font size <code>ComboBox</code> is made editable as follows:
*
* <pre>
*     sizeCombo.setEditable(true);
* </pre>
*
* This allows the font size to be changed two ways:
* by selecting from the combobox list, or by 
* directly entering the desired font size.<p>
*
* For this demo, we require the font size to be an integer
* in the range 1-500.  If the user makes an invalid input,
* and then presses ENTER or TAB,
* things get a little messy.  The approach taken is to keep the focus on the
* size combo box editor until the input is corrected.  This is
* accomplished by<p>
*
* <pre>
*     cb.getEditor().getEditorComponent().requestFocus();
* </pre>
*
* where <code>cb</code> is the size combo box object,
* <code>getEditor</code> returns a
* <code>ComboBoxEditor</code> object,
* <code>getEditorComponent</code> returns the
* <code>Component</code> used for the combo box editor, and
* <code>requestFocus</code> requests that the focus remain on
* the editor (rather than advancing to the next component in
* the focus sequence).<p> 
*
* Unfortunately, there isn't an obvious way to perform
* input validation in concert with navigation via mouse clicks.  In
* other words, if the user types in a font size, then clicks
* in another component (without pressing ENTER or TAB),
* the input is not validated, nor is the message updated.
* The customary way to accommodate this is using a focus listener
* (see <code>DemoTextField.java</code> for an example).  Comboboxes
* fire action events when the user selects an item with the mouse
* or presses TAB or ENTER
* (for editable combo boxes).  The 'normal' way to process input via
* a combo box is via the <code>actionPerformed</code> method, and
* this is the technique used here.  Athough, an
* <code>addFocusListener</code> method exists for
* <code>JComboBox</code>, its use is not recommended because
* a combo box is a compound component (not an atomic component)
* and Look & Feel side effects may occur.  (This is noted in
* the Sun's tutorial notes on <code>JComboBox</code>).<p>
*
* <b>Custom Renderer</b><br>
* Rather than tweeking the default combobox renderer,
* as we did with the size combobox to center-align the entries,
* another option is to replace the renderer
* with a custom renderer.  This approach is used here with the
* font combobox:<p>
*
* <pre>
*     fontCombo.setRenderer(new CustomRenderer());
* </pre>
*
* Our <code>CustomRenderer</code> class is defined here as an inner
* class.  (Consult the source code for all the details.)  It extends
* <code>JLabel</code> and implements the <code>ListCellRenderer</code>
* interface.
* 
* In defining a custom renderer,
* we have more control over the appearance
* of the entries in the <code>JComboBox</code>.
* As seen here with
* the font combobox, the family names
* appear rendered in the named font.  In this demo,
* the font combobox displays all(!) the fonts available on the
* system, which happens to be quite a few.  Unfortunately, the
* application window is quite slow to appear due to the time taken to render
* the entries.<p>
*
*
* Finally, we place this demo's message component
* -- a <code>JLabel</code> -- within
* a scrollpane.  If a very large font size is entered in the editable
* <code>JComboBox</code> for the size, then scrollbars automatically appear,
* if necessary.<p>
*
* Screen snap... (launching)<br>
* <center><img src="DemoComboBox2-1.gif"></center><p>
*
* Screen snap... (initial application window)<br>
* <center><img src="DemoComboBox2-2.gif"></center><p>
*
* Screen snap... (after a few changes)<br>
* <center><img src="DemoComboBox2-3.gif"></center><p>
*
* @see <a href="DemoComboBox2.java">source code</a>
* @author Scott MacKenzie, 2002
*/
public class DemoComboBox2
{
   public static void main(String[] args)
   {
      // use look and feel for my system (Win32)
      try {
         UIManager.setLookAndFeel(
            UIManager.getSystemLookAndFeelClassName());
      } catch (Exception e) {}

      DemoComboBox2Frame frame = new DemoComboBox2Frame();
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.setTitle("DemoComboBox2");
      frame.pack();
      frame.show();
   }
}

class DemoComboBox2Frame extends JFrame
implements ActionListener, ItemListener
{
   final int DEFAULT_STYLE = Font.PLAIN;
   final int DEFAULT_SIZE = 22;
   final int DEFAULT_SIZE_INDEX = 3;
   final String[] SZ = { "10", "14", "18", "22", "26", "32", "38", "48" };

   private JTextField message;
   private JButton exitButton;
   private JButton restoreButton;
   private JCheckBox italicCheckBox;
   private JCheckBox boldCheckBox;
   private JComboBox sizeCombo;
   private JComboBox fontCombo;

   private String fontFamily;
   private int fontSize;
   private int fontStyle;
   private String[] fontList;
   private String defaultFamily;
   private int defaultFamilyIndex;

   public DemoComboBox2Frame()
   {
      // fill a string array with the names of all the fonts on this system

      GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
      fontList = ge.getAvailableFontFamilyNames();

      System.out.println("Number of fonts = " + fontList.length);
      System.out.println("Rendering list of font family names... (please wait)"); 

      // find index of default font family (let's try for 'serif')

      defaultFamilyIndex = -1;
      for (int i = 0; i < fontList.length; ++i)
      {
         if (fontList[i].toLowerCase().equals("serif"))
         {
            defaultFamilyIndex = i;
            break;
         }
      }

      if (defaultFamilyIndex == -1) // not found!
      {
         JOptionPane.showMessageDialog(this,
            "Default font family ('serif') not found!\n" +
            "Will use '" + fontList[0] + "' as default.",
            "Information Message",
            JOptionPane.INFORMATION_MESSAGE
         );
         defaultFamilyIndex = 0;
      }
      defaultFamily = fontList[defaultFamilyIndex];

      fontFamily = defaultFamily;
      fontStyle = DEFAULT_STYLE;
      fontSize = DEFAULT_SIZE;
                                
      // ----------------------------------
      // construct and configure components
      // ----------------------------------

      message = new JTextField("Hello Java World");
      message.setFont(new Font(defaultFamily, DEFAULT_STYLE, DEFAULT_SIZE));
      message.setEditable(false);
      message.setBackground(Color.pink);
      message.setForeground(Color.blue);
      message.setHorizontalAlignment(SwingConstants.CENTER);

      JScrollPane sp = new JScrollPane(message);
      sp.setPreferredSize(new Dimension(300, 150));

      restoreButton = new JButton("Restore");
      exitButton = new JButton("Exit");

      italicCheckBox = new JCheckBox("Italic");
      boldCheckBox = new JCheckBox("Bold");

      sizeCombo = new JComboBox(SZ);
      sizeCombo.setEditable(true);

      // center align the entries

      // Note: this must be done twice, once for the entries in the popup
      // list, which are instances of JLabel, and once for the selected
      // item, which is an instance of JTextField (because the JComboBox
      // is editable).

      ((JLabel)sizeCombo.getRenderer()).setHorizontalAlignment(SwingConstants.CENTER);
      ((JTextField)sizeCombo.getEditor().getEditorComponent()).setHorizontalAlignment(JTextField.CENTER);

      sizeCombo.setSelectedIndex(DEFAULT_SIZE_INDEX);
      sizeCombo.setPreferredSize(new Dimension(75, 25));

      fontCombo = new JComboBox(fontList);
      fontCombo.setRenderer(new CustomRenderer());
      fontCombo.setSelectedIndex(defaultFamilyIndex);
      fontCombo.setPreferredSize(new Dimension(150, 25));

      // -------------
      // add listeners
      // -------------

      restoreButton.addActionListener(this);
      exitButton.addActionListener(this);
      italicCheckBox.addItemListener(this);
      boldCheckBox.addItemListener(this);
      sizeCombo.addActionListener(this);
      fontCombo.addActionListener(this);

      // ------------------
      // arrange components
      // ------------------

      // add components to panels
      
      JPanel commandPanel = new JPanel();
      commandPanel.setLayout(new GridLayout(2, 1));
      commandPanel.add(restoreButton);
      commandPanel.add(exitButton);
      commandPanel.setBorder(new TitledBorder(new EtchedBorder(), "Commands"));

      Dimension d = commandPanel.getPreferredSize();

      JPanel sizePanel = new JPanel();
      sizePanel.setLayout(new GridLayout(2, 1));
      sizePanel.add(sizeCombo);
      sizePanel.setBorder(new TitledBorder(new EtchedBorder(), "Size"));
      sizePanel.setPreferredSize(d);

      JPanel stylePanel = new JPanel();
      stylePanel.setLayout(new GridLayout(2, 1));
      stylePanel.add(italicCheckBox);
      stylePanel.add(boldCheckBox);
      stylePanel.setBorder(new TitledBorder(new EtchedBorder(), "Style"));
      stylePanel.setPreferredSize(d);

      JPanel fontPanel = new JPanel();
      fontPanel.setLayout(new GridLayout(2, 1));
      fontPanel.add(fontCombo);
      fontPanel.setBorder(new TitledBorder(new EtchedBorder(), "Font"));
      fontPanel.setPreferredSize(new Dimension(d.width * 2, d.height));

      // arrange panels

      JPanel southPanel = new JPanel();
      southPanel.add(commandPanel);
      southPanel.add(sizePanel);
      southPanel.add(stylePanel);
      southPanel.add(fontPanel);

      // add panels to content pane

      Container contentPane = getContentPane();
      contentPane.add(sp, "Center");
      contentPane.add(southPanel, "South");
   }

   // -------------------------------
   // implement ActionListener method
   // -------------------------------

   public void actionPerformed(ActionEvent ae)
   {
      Object source = ae.getSource();

      // 'Command' - check if a command button was pressed
      if (source == restoreButton)
      {
         fontFamily = fontList[defaultFamilyIndex];
         fontStyle = DEFAULT_STYLE;
         fontSize = DEFAULT_SIZE;
         sizeCombo.setSelectedIndex(DEFAULT_SIZE_INDEX);
         italicCheckBox.setSelected(false);
         boldCheckBox.setSelected(false);
         fontCombo.setSelectedIndex(defaultFamilyIndex);
      }
      else if (source == exitButton)
         System.exit(0);

      // 'Size' - check if the font size was changed via the combobox
      else if (source == sizeCombo)
      {
         int tmp = this.getFontSize(sizeCombo);
         if (tmp == -1)
            return;
         else
            fontSize = tmp;
      }

      // 'Font' - check if the font family was changed via the combobox
      else if (source == fontCombo)
         fontFamily = fontList[fontCombo.getSelectedIndex()];

      // update message font, size, and style
      message.setFont(new Font(fontFamily, fontStyle, fontSize));
   }

   // -----------------------------
   // implement ItemListener method
   // -----------------------------

   public void itemStateChanged(ItemEvent ie)
   {
      Object source = ie.getSource();

      // 'Style' - check if the font style was change via a checkbox
      if (source == italicCheckBox)
      {
         if (italicCheckBox.isSelected())
            fontStyle = fontStyle | Font.ITALIC;  // turn italic on
         else
            fontStyle = fontStyle & ~Font.ITALIC; // turn italic off
      }
      else if (source == boldCheckBox)
      {
         if (boldCheckBox.isSelected())
            fontStyle = fontStyle | Font.BOLD;    // turn bold on
         else
            fontStyle = fontStyle & ~Font.BOLD;   // turn bold off
      }

      // update message font, size, and style
      message.setFont(new Font(fontFamily, fontStyle, fontSize));
   }

   // -------------
   // other methods
   // -------------

   /** Get the size of the font from the combo box.
   * @param cb the combo box
   * @return a int equal to the font size, or -1 if invalid.
   * Note a valid font size is an integer in the range 1-500
   */
   private int getFontSize(JComboBox cb)
   {
      String userInput = (String)cb.getSelectedItem();
      boolean ok = true;
      int fontSize = -1;
      try
      {
         fontSize = Integer.parseInt(userInput);
      } catch (NumberFormatException nfe)
      {
         ok = false;
      }

      if (fontSize < 1 || fontSize > 500)
      {
         ok = false;
         fontSize = -1; // indicates invalid
      }

      if (!ok)
      {
         Toolkit.getDefaultToolkit().beep();
         JOptionPane.showMessageDialog(this,
            "Please enter an integer in the range 1-500!",
            "Invalid Input",
            JOptionPane.ERROR_MESSAGE
         );

         // keep focus on the combobox editor until input is corrected

         cb.getEditor().getEditorComponent().requestFocus();
      }

      return fontSize; // returns -1 if invalid
   }

   // -----------
   // inner class
   // -----------

   class CustomRenderer extends JTextField implements ListCellRenderer
   {
      public CustomRenderer()
      {
         this.setOpaque(true);
         this.setHorizontalAlignment(LEFT);
         this.setBorder(null);
      }

      public Component getListCellRendererComponent(JList list, Object value,
         int index, boolean isSelected, boolean cellHasFocus)
      {
         if (isSelected)
         {
            setBackground(list.getSelectionBackground());
            setForeground(list.getSelectionForeground());
         } else
         {
            setBackground(list.getBackground());
            setForeground(list.getForeground());
         }

         String fontName = (String)value;
         this.setText(fontName);

         // The following line is the 'clincher'.  This will cause
         // the font name to be rendered in the named font.

         this.setFont(new Font(fontName, Font.PLAIN, 14));

         return this;
      }
   }
}

