Running on Java 22-ea+27-2262 (Preview)
Home of The JavaSpecialists' Newsletter

145TristateCheckBox Revisited

Author: Dr. Heinz M. KabutzDate: 2007-05-25Java Version: 5Category: GUI
 

Abstract: The Tristate Checkbox is widely used to represent an undetermined state of a check box. In this newsletter, we present a new version of this popular control, retrofitted to Java 5 and 6.

 

Welcome to the 145th issue of The Java(tm) Specialists' Newsletter sent from Korakies, Chania! When we moved to the Island of Crete from South Africa, we brought our Ed Seiler upright piano along. You should have seen the movers carry that baby up the stairs! We had intended to let it settle for a month or two, then tune it. Helene spoke to Maxi's piano teacher yesterday and discovered that the piano tuner is coming to the island in October. That's right, October! He visits here from Athens, tunes all the pianos and then scurries back home. So for the next, um, five months, we will have to endure an out-of-tune piano!

As mentioned in the previous newsletter, from July 2007, I will be offering Java code reviews for your team to get an expert's viewpoint of your Java system. Please take note and speak to your manager about this opportunity.

This last week, I added local search functionality to our newsletter archive. With over 157 newsletters and follow-ups, it is becoming increasingly difficult to find back issues, many of which I still dig out when I cannot figure something out. The search facility is available on the main archive page, together with the abstracts and titles of all our 157 newsletters and followups.

Unfortunately, due to my ambivalence in choosing domain names - we went from www.smotricz.com/kabutz to www.javaspecialists.co.za to www.cretesoft.com and now finally to www.javaspecialists.EU - poor Google is having a hard time keeping up. I have resubmitted a sitemap, but it will likely take a few days before the site is correctly indexed.

Then I did something else a few months ago, which was really really dumb. I have a mapper class in my website that automatically translates URLs of the format https://www.javaspecialists.eu/archive/Issue###.html into the servlet URL that is needed to display this. It saves me from having to write the mapping into the web.xml file. It also allows me to support older versions of URLs with minimal effort. However, I forgot to change the status code of the result, which at that point had been set to 404 (page not found). This would have been invisible to humans reading the pages, but web crawlers would have a serious issue with that.

javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.

TristateCheckBox Revisited

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.

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.

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.

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.

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());
    }
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.pack();
    frame.setVisible(true);
  }

  private static JPanel makePanel() {
    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.

Kind regards from Crete

Heinz

P.S. The next few weeks will be quite busy as Helene and our baby Evangeline go to England. I have the task of keeping Maximilian and Constance well fed and clean until her return. Don't expect any newsletters in the next three weeks :-) We already have plans to stay up late, eat crisps, watch movies, bunk school, catch fish, etc. Maybe we will even go camping.

 

Comments

We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)

When you load these comments, you'll be connected to Disqus. Privacy Statement.

Related Articles

Browse the Newsletter Archive

About the Author

Heinz Kabutz Java Conference Speaker

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Superpack '23

Superpack '23 Our entire Java Specialists Training in one huge bundle more...

Free Java Book

Dynamic Proxies in Java Book
Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

We can help make your Java application run faster and trouble-shoot concurrency and performance bugs...