Making a VisAD widget collaborative

When writing a widget for a standalone VisAD application, the usual procedure is to create a Swing widget and, when that widget changes, change the Display appropriately.

For example, here's a code fragment which adds the ability to turn the VisAD box on and off:
  1. JCheckBox cb = new JCheckBox("Box", true);
  2. cb.addItemListener(new ItemListener() {
  3.   public void itemStateChanged(ItemEvent E){
  4.     try {
  5.       boolean isSelected = (E.getStateChange() == ItemEvent.SELECTED);
  6.       display.getDisplayRenderer().setBoxOn(isSelected);
  7.     } catch (VisADException ve) {
  8.       System.err.println(ve.getMessage());
  9.     } catch (RemoteException re) {
  10.       System.err.println(re.getMessage());
  11.     }
  12.   }
  13. });
Line 1 creates a standard checkbox widget.
Line 2 tells the widget to pass any change events to the listener method in lines 3-12.
The meat of that listener method is lines 5 and 6; the other lines are there simply to catch exceptions.
Line 5 check to see whether the box is checked or unchecked.
Line 6 specifies that the box be turned on or off, based to the value computed in line 5.

When writing a collaborative graphical widget, things are a bit more complicated. If the box widget above were used in a collaborative application, a remote change would toggle the box visibility (if the box were initially visible, for example, and a remote user turned it off) but the checkbox widget would remain checked.

In VisAD, Displays keep much of their state in Controls and ScalarMaps. Applications are notified of remote changes via listener methods on those Controls and ScalarMaps.

Box visibility is tracked by the RendererControl, so the application needs to first fetch that Control and then add a method to listen for changes to that Control.

The Display's RendererControl can be fetched via the getControl() method which takes the Control's class as an argument and returns the first (and in this case, only) Control of that type:
      rendCtl = (RendererControl )dpy.getControl(RendererControl.class);
    
The RendererControl listener method would look like:
  1. rendCtl.addControlListener(new ControlListener() {
  2.   public void controlChanged(ControlEvent evt)
  3.     throws RemoteException, VisADException
  4.   {
  5.     boolean rState = rendCtl.getBoxOn();
  6.     boolean jState = cb.isSelected();
  7.     if (jState != rState) {
  8.       cb.setSelected(rState);
  9.     }
  10.   }
  11. });
Line 1 tells the RendererControl to pass any change events to the listener method in lines 2-10.
Lines 2-4 are the standard declaration for a visad.ControlListener.
Line 5 sets rState to true if the Box is currently visible.
Line 6 sets jState to true if the checkbox is currently checked.
Lines 7 and 8 set the checkbox to the proper state if it doesn't currently reflect the box visibility.

The initial box widget creates a JCheckBox widget which is already checked, because VisAD Displays are created with a visible box. This isn't a valid assumption in a collaborative application, because a client can join a session at any point, so the defaults may have been changed.

To fix this, the graphics widget should be initialized using the current Control state as a guide. For the box widget, the initialization code would be:
      cb.setSelected(rendCtl.getBoxOn());
    
Adding all the previous code results in a box widget which works the same on standalone, server and client applications and which is initialized properly no matter when it is started. The final (optional) step would be to move all this code into a separate class, so it can easily be reused in other applications.

Here's the code as a BoxWidget class:
      import java.awt.event.*;
      import java.rmi.RemoteException;
      import javax.swing.JCheckBox;
      import visad.*;

      /**
       * A widget which allows a user to turn the rendered box on and off
       */
      public class BoxWidget
        extends JCheckBox
      {
        RendererControl ctl;

        public BoxWidget(LocalDisplay dpy)
        {
          super("Box", true);

          // get Display's RendererControl
          ctl = (RendererControl )dpy.getControl(RendererControl.class);

          // make sure it reflects the current box visibility
          setSelected(ctl.getBoxOn());

          // listen for changes to the RendererControl
          ctl.addControlListener(new ControlListener() {
              public void controlChanged(ControlEvent evt)
                throws RemoteException, VisADException
              {
                // get box visibility
                boolean drState = ctl.getBoxOn();

                // if checkbox state doesn't match box visibility...
                if (isSelected() != drState) {
                  // ...set it appropriately
                  setSelected(drState);
                }
              }
            });

          // listen for changes to the JCheckBox
          addItemListener(new ItemListener() {
              public void itemStateChanged(ItemEvent evt){
                try {
                  // get the checkbox state
                  boolean jState = evt.getStateChange() == ItemEvent.SELECTED;

                  // set box visibility appropriately
                  ctl.setBoxOn(jState);
                } catch (VisADException ve) {
                  System.err.println(ve.getMessage());
                } catch (RemoteException re) {
                  System.err.println(re.getMessage());
                }
              }
            });
        }
      }
    

Last modified: Tue Jul 11 15:01:53 CDT 2000