TristateCheckBox Revisited (JAVA)

Have you ever written a piece of software and not received bug reports for it? If so, then make sure that your clients are actually running the code!
Three and a half years ago, I published a TristateCheckBox that you can use in Swing applications and applets. Unlike a normal check box it had three states, SELECTED, DESELECTED and INDETERMINATE.
Even though it is just a small little GUI class, I have had requests from no less than eight institutes, including a Fortune 500 company and a major university, asking whether they could use the class. In addition, several readers have sent corrections and additions to the class.
The most recent was by Mark Zellers from Adaptive Planning, informing me that there was a problem with the TristateCheckBox. His description of the bug struck me as odd, so I ran my old code again. It looks like the behaviour changed in the move over to Java 5. The check box did not retain its INDETERMINATE state when it lost focus.
Unfortunately, none of the proposed fixes covered all the issues that needed to be addressed. Even after spending the entire day reading through related emails and coming up with a better solution, I am convinced that there are still snafus that I have not thought of yet.
The best solution was sent by David Wright, who is working on the Superficial framework for GUIs. In his last email to me, David wrote: Trying to produce a really robust TristateCheckBox has turned out to be quite a challenge and I'm still not sure I've covered all the bases. However, I would be delighted if you wanted to publish this or a further improved version.
Several readers also sent me minor corrections to do with using enums for the states, so let us start with the states. Here, we have a very simple state machine that indicates what the next state would be. This works in a chain of Selected -> Indeterminate -> Deselected -> Selected.

package eu.javaspecialists.tjsn.gui;

public enum TristateState {
  SELECTED {
    public TristateState next() {
      return INDETERMINATE;
    }
  },
  INDETERMINATE {
    public TristateState next() {
      return DESELECTED;
    }
  },
  DESELECTED {
    public TristateState next() {
      return SELECTED;
    }
  };

  public abstract TristateState next();
}
  
In David Wright's approach, he subclassed the TristateButtonModel from ToggleButtonModel, rather than try to decorate the model. This allows us to reuse the model for other components, for example, a TristateRadioButton (not shown here).
A point worth mentioning is that the itemChanged event is typically only fired with the checkbox is either selected or deselected (not during the intermediate states). We have to manually fire off the events in the setState() method.
package eu.javaspecialists.tjsn.gui;

import javax.swing.JToggleButton.ToggleButtonModel;
import java.awt.event.ItemEvent;

public class TristateButtonModel extends ToggleButtonModel {
  private TristateState state = TristateState.DESELECTED;

  public TristateButtonModel(TristateState state) {
    setState(state);
  }

  public TristateButtonModel() {
    this(TristateState.DESELECTED);
  }

  public void setIndeterminate() {
    setState(TristateState.INDETERMINATE);
  }

  public boolean isIndeterminate() {
    return state == TristateState.INDETERMINATE;
  }

  // Overrides of superclass methods
  public void setEnabled(boolean enabled) {
    super.setEnabled(enabled);
    // Restore state display
    displayState();
  }

  public void setSelected(boolean selected) {
    setState(selected ?
        TristateState.SELECTED : TristateState.DESELECTED);
  }

  // Empty overrides of superclass methods
  public void setArmed(boolean b) {
  }

  public void setPressed(boolean b) {
  }

  void iterateState() {
    setState(state.next());
  }

  private void setState(TristateState state) {
    //Set internal state
    this.state = state;
    displayState();
    if (state == TristateState.INDETERMINATE && isEnabled()) {
      // force the events to fire

      // Send ChangeEvent
      fireStateChanged();

      // Send ItemEvent
      int indeterminate = 3;
      fireItemStateChanged(new ItemEvent(
          this, ItemEvent.ITEM_STATE_CHANGED, this,
          indeterminate));
    }
  }

  private void displayState() {
    super.setSelected(state != TristateState.DESELECTED);
    super.setArmed(state == TristateState.INDETERMINATE);
    super.setPressed(state == TristateState.INDETERMINATE);

  }

  public TristateState getState() {
    return state;
  }
}
  
We reference this model from within the TristateCheckbox class. Users can either figure out the state using the isSelected() and isIndeterminate() methods or by calling the getState() method.
package eu.javaspecialists.tjsn.gui;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.plaf.ActionMapUIResource;
import java.awt.*;
import java.awt.event.*;

public final class TristateCheckBox extends JCheckBox {
  // Listener on model changes to maintain correct focusability
  private final ChangeListener enableListener =
      new ChangeListener() {
        public void stateChanged(ChangeEvent e) {
          TristateCheckBox.this.setFocusable(
              getModel().isEnabled());
        }
      };

  public TristateCheckBox(String text) {
    this(text, null, TristateState.DESELECTED);
  }

  public TristateCheckBox(String text, Icon icon,
                          TristateState initial) {
    super(text, icon);

    //Set default single model
    setModel(new TristateButtonModel(initial));

    // override action behaviour
    super.addMouseListener(new MouseAdapter() {
      public void mousePressed(MouseEvent e) {
        TristateCheckBox.this.iterateState();
      }
    });
    ActionMap actions = new ActionMapUIResource();
    actions.put("pressed", new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        TristateCheckBox.this.iterateState();
      }
    });
    actions.put("released", null);
    SwingUtilities.replaceUIActionMap(this, actions);
  }

  // Next two methods implement new API by delegation to model
  public void setIndeterminate() {
    getTristateModel().setIndeterminate();
  }

  public boolean isIndeterminate() {
    return getTristateModel().isIndeterminate();
  }

  public TristateState getState() {
    return getTristateModel().getState();
  }

  //Overrides superclass method
  public void setModel(ButtonModel newModel) {
    super.setModel(newModel);

    //Listen for enable changes
    if (model instanceof TristateButtonModel)
      model.addChangeListener(enableListener);
  }

  //Empty override of superclass method
  public void addMouseListener(MouseListener l) {
  }

  // Mostly delegates to model
  private void iterateState() {
    //Maybe do nothing at all?
    if (!getModel().isEnabled()) return;

    grabFocus();

    // Iterate state
    getTristateModel().iterateState();

    // Fire ActionEvent
    int modifiers = 0;
    AWTEvent currentEvent = EventQueue.getCurrentEvent();
    if (currentEvent instanceof InputEvent) {
      modifiers = ((InputEvent) currentEvent).getModifiers();
    } else if (currentEvent instanceof ActionEvent) {
      modifiers = ((ActionEvent) currentEvent).getModifiers();
    }
    fireActionPerformed(new ActionEvent(this,
        ActionEvent.ACTION_PERFORMED, getText(),
        System.currentTimeMillis(), modifiers));
  }

  //Convenience cast
  public TristateButtonModel getTristateModel() {
    return (TristateButtonModel) super.getModel();
  }
}
  
We also have a test case for this class, using the various look and feels installed on your system. Note that this approach does not work well on the Motif L&F.
package eu.javaspecialists.tjsn.gui;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class TristateCheckBoxTest {
  public static void main(String args[]) throws Exception {
    JFrame frame = new JFrame("TristateCheckBoxTest");
    frame.setLayout(new GridLayout(0, 1, 15, 15));
    UIManager.LookAndFeelInfo[] lfs =
        UIManager.getInstalledLookAndFeels();
    for (UIManager.LookAndFeelInfo lf : lfs) {
      System.out.println("Look&Feel " + lf.getName());
      UIManager.setLookAndFeel(lf.getClassName());
      frame.add(makePanel(lf));
    }
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.pack();
    frame.setVisible(true);
  }

  private static JPanel makePanel(UIManager.LookAndFeelInfo lf) {
    final TristateCheckBox tristateBox = new TristateCheckBox(
        "Tristate checkbox");
    tristateBox.addItemListener(new ItemListener() {
      public void itemStateChanged(ItemEvent e) {
        switch(tristateBox.getState()) {
          case SELECTED:
            System.out.println("Selected"); break;
          case DESELECTED:
            System.out.println("Not Selected"); break;
          case INDETERMINATE:
            System.out.println("Tristate Selected"); break;
        }
      }
    });
    tristateBox.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        System.out.println(e);
      }
    });
    final JCheckBox normalBox = new JCheckBox("Normal checkbox");
    normalBox.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        System.out.println(e);
      }
    });

    final JCheckBox enabledBox = new JCheckBox("Enable", true);
    enabledBox.addItemListener(new ItemListener() {
      public void itemStateChanged(ItemEvent e) {
        tristateBox.setEnabled(enabledBox.isSelected());
        normalBox.setEnabled(enabledBox.isSelected());
      }
    });

    JPanel panel = new JPanel(new GridLayout(0, 1, 5, 5));
    panel.add(new JLabel(UIManager.getLookAndFeel().getName()));
    panel.add(tristateBox);
    panel.add(normalBox);
    panel.add(enabledBox);
    return panel;
  }
}
  
Please try it out and let me know if it causes any problems or unexpected behaviour on your system.

created by Dr. Heinz M. Kabutz

0 komentar:

Posting Komentar

Twitter Delicious Facebook Digg Stumbleupon Favorites More

 
This Theme Modified by Kapten Andre based on Structure Theme from MIT-style License by Jason J. Jaeger