ipetrik
ipetrik

Reputation: 2044

Implementing JTree nodes with radio/check boxes

I'm trying to achieve an elegant tree representation in which certain types of nodes are displayed as panels containing text, a radio button, and a check box. Below is a picture of what I have currently and the code that generates it. However there are a few problems that just make it feel dirty, and I'm not sure the best way to get around them.

Example Tree

public class DatasetTree extends JTree {

  public DatasetTree(String name) {
    super(new DatasetTreeModel(name));
    getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
    DatasetTreeCellRenderer renderer = new DatasetTreeCellRenderer();
    renderer.setOpenIcon(null);
    renderer.setClosedIcon(null);
    renderer.setLeafIcon(null);
    setCellRenderer(renderer);
    setEditable(true);
    PanelCellEditor editor = new PanelCellEditor(this, renderer);
    setCellEditor(editor);
    setShowsRootHandles(true);
    setRootVisible(false);
  }

  public DatasetTreeModel getDatasetModel() {
    return (DatasetTreeModel) treeModel;
  }

  public static class DatasetTreeCellRenderer extends DefaultTreeCellRenderer {

    public DatasetTreeCellRenderer() {

    }

    @Override
    public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel,
        boolean expanded, boolean leaf, int row, boolean hasFocus) {

      if ((value != null) && (value instanceof DatasetHandle)) {
        DatasetHandle h = (DatasetHandle) value;
        DatasetCellPanel line = new DatasetCellPanel(h);
        if (sel) {
          line.setBackground(getBackgroundSelectionColor());
          line.setForeground(getTextSelectionColor());
        } else {
          line.setBackground(getBackgroundNonSelectionColor());
          line.setForeground(getTextNonSelectionColor());
        }
        return line;
      }
      return super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
    }
  }

  public static class DatasetCellPanel extends JPanel {

    private final JLabel lblName, lblType, lblom, lbldata, lblimages, lblspectra;
    private boolean observable;
    private boolean orientable;

    private JRadioButton omButton;
    private JCheckBox dataSelectBox;

    /**
     * Create the panel.
     */
    public DatasetCellPanel(DatasetHandle h) {
      super();
      setBackground(Color.WHITE);
      FileData fd = h.getFileData();
      String name = fd.getFileName();
      boolean observable = (fd instanceof ObservableData);
      boolean orientable = (fd instanceof Orientable);
      String typeName = fd.getClass().getSimpleName();
      lblName = new JLabel("");
      lblType = new JLabel("");
      lblom = new JLabel("[om]");
      lbldata = new JLabel("[data]");
      lblimages = new JLabel("[images]");
      lblspectra = new JLabel("[spectra]");

      JRadioButton omButton = new JRadioButton("");
      JCheckBox dataSelectBox = new JCheckBox("");

      setLayout(new BoxLayout(this, BoxLayout.X_AXIS));

      lblName.setText(name);
      lblName.setMinimumSize(new Dimension(100, 8));
      lblName.setPreferredSize(new Dimension(100, 16));
      lblName.setMaximumSize(new Dimension(100, 64));
      add(lblName);
      add(Box.createRigidArea(new Dimension(5, 0)));

      lblType.setText(typeName);
      lblType.setMinimumSize(new Dimension(100, 8));
      lblType.setPreferredSize(new Dimension(100, 16));
      lblType.setMaximumSize(new Dimension(100, 64));
      add(lblType);
      add(Box.createRigidArea(new Dimension(5, 0)));

      if (orientable) {
        omButton = h.getLatticeButton();
      } else {
        lblom.setForeground(UIManager.getColor("Label.disabledForeground"));
        omButton.setEnabled(false);
      }
      add(lblom);
      add(omButton);
      add(Box.createRigidArea(new Dimension(5, 0)));

      if (observable) {
        dataSelectBox = h.getDataButton();
      } else {
        lbldata.setForeground(UIManager.getColor("Label.disabledForeground"));
        dataSelectBox.setEnabled(false);
      }
      add(lbldata);
      add(dataSelectBox);
      add(Box.createRigidArea(new Dimension(5, 0)));

      add(lblimages);
      add(Box.createRigidArea(new Dimension(5, 0)));
      add(lblspectra);

    }

    public void addListeners(EventListener l) {

    }

    @Override
    public void setForeground(Color fg) {
      if (lblName != null) {
        lblName.setForeground(fg);
      }
      if (lblType != null) {
        lblType.setForeground(fg);
      }
      if (observable && (lbldata != null)) {
        lbldata.setForeground(fg);
      }
      if (orientable && (lblom != null)) {
        lblom.setForeground(fg);
      }
      if (lblimages != null) {
        lblimages.setForeground(fg);
      }
      if (lblspectra != null) {
        lblspectra.setForeground(fg);
      }
      super.setForeground(fg);
    }

    @Override
    public void setBackground(Color bg) {
      if (omButton != null) {
        omButton.setBackground(bg);
      }
      if (dataSelectBox != null) {
        dataSelectBox.setBackground(bg);
      }
      super.setBackground(bg);
    }

  }

  public static class PanelCellEditor extends AbstractCellEditor implements TreeCellEditor {

    Object value;
    private JTree tree;
    private DefaultTreeCellRenderer renderer;

    public PanelCellEditor(JTree tree, DefaultTreeCellRenderer renderer) {
      this.tree = tree;
      this.renderer = renderer;
    }

    @Override
    public Object getCellEditorValue() {
      return value;
    }

    // FIXME: Redraw all in group when one is edited
    @Override
    public Component getTreeCellEditorComponent(JTree tree, Object value, boolean sel,
        boolean expanded, boolean leaf, int row) {
      this.value = value;
      if ((value != null) && (value instanceof DatasetHandle)) {
        DatasetHandle h = (DatasetHandle) value;
        DatasetCellPanel line = new DatasetCellPanel(h);
        if (sel) {
          line.setBackground(renderer.getBackgroundSelectionColor());
          line.setForeground(renderer.getTextSelectionColor());
        } else {
          line.setBackground(renderer.getBackgroundNonSelectionColor());
          line.setForeground(renderer.getTextNonSelectionColor());
        }
        return line;
      }
      return renderer.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, false);
    }
  }

}

(1) The buttons/boxes are only responsive after editing is enabled by clicking on the node once. Before that, the button/box doesn't glow on mouse over.

(2) The radio buttons for each group of nodes under a parent are in a single button group. But when I select one, the visual representation of the other does not get updated to reflect that it was deselected, until I click somewhere in it to 'edit' it.

(3) In general, this standard type of tree, where the nodes are just dummy objects, not actual components, seems improperly suited for this, but I can't think of a better alternative that allows me to group these objects, select individual nodes (either leafs or parents), and have each leaf contain check boxes/buttons that work properly.

I'm open to suggestions of alternative solutions.

EDIT:

Tried using Outline, which seems closer to what I want, but having technical issues. I followed the example here. This is what I get:

Outline attempt 1

As you can see, the buttons are not displayed properly. Here is my RowModel:

public class DatasetOutlineRowModel implements RowModel {

  @Override
  public Class getColumnClass(int column) {
    switch (column) {
      case 0:
        return JRadioButton.class;
      case 1:
        return JCheckBox.class;
      case 2:
        return String.class;
      case 3:
        return String.class;
      default:
        assert false;
    }
    return null;
  }

  @Override
  public int getColumnCount() {
    return 4;
  }

  @Override
  public String getColumnName(int column) {
    switch (column) {
      case 0:
        return "OM";
      case 1:
        return "Data";
      case 2:
        return "Images";
      case 3:
        return "Spectra";
      default:
        assert false;
    }
    return null;
  }

  @Override
  public Object getValueFor(Object node, int column) {
    if (!(node instanceof DatasetHandle))
      return null;
    DatasetHandle handle = (DatasetHandle) node;
    switch (column) {
      case 0:
        return handle.getLatticeButton();
      case 1:
        return handle.getDataButton();
      case 2:
        return "";
      case 3:
        return "";
      default:
        assert false;
    }
    return null;
  }

  @Override
  public boolean isCellEditable(Object arg0, int arg1) {
    return false;
  }

  @Override
  public void setValueFor(Object arg0, int arg1, Object arg2) {
    // TODO Auto-generated method stub

  }

}

Upvotes: 0

Views: 713

Answers (1)

ipetrik
ipetrik

Reputation: 2044

OK, so I finally figured out how to achieve this basing it on the way JTable handles boolean cells. I created an exclusive boolean selection renderer to draw the JRadioButton and set up the tree node to make sure exclusive selection is maintained. I also overrode editingStopped to update all the cells in the column if one of the cells was edited. There are probably ways to improve this, but it works for what I need. Thanks for the guidance.

enter image description here

Here is my code:

DatasetOutline class

public class DatasetOutline extends Outline {

  public DatasetOutline(DatasetTreeModel mdl) {
    setRenderDataProvider(new DatasetRenderProvider());
    setRootVisible(false);
    setShowGrid(false);
    setIntercellSpacing(new Dimension(0, 0));
    setModel(DefaultOutlineModel.createOutlineModel(mdl, new DatasetOutlineRowModel(), true,
        "Dataset"));
    getColumnModel().getColumn(1).setCellRenderer(new ExclusiveBooleanRenderer());
    getColumnModel().getColumn(1).setCellEditor(new ExclusiveBooleanEditor());
    // [snip]
    getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
  }

  // Update the entire column of the conditional boolean if one is changed
  @Override
  public void editingStopped(ChangeEvent e) {
    super.editingStopped(e);
    if (e.getSource() instanceof ExclusiveBooleanEditor) {
      tableChanged(new TableModelEvent(getModel(), 0, getRowCount(), 1, TableModelEvent.UPDATE));
    }
  }
}

DatasetOutlineRowModel class

public class DatasetOutlineRowModel implements RowModel {

  @Override
  public Class getColumnClass(int column) {
    switch (column) {
      case 0:
        return Boolean.class;
      case 1:
        return Boolean.class;
      case 2:
        return String.class;
      case 3:
        return String.class;
      default:
        assert false;
    }
    return null;
  }

 // [snip]

  @Override
  public Object getValueFor(Object node, int column) {
    if (!(node instanceof DatasetHandle))
      return null;
    DatasetHandle handle = (DatasetHandle) node;
    switch (column) {
      case 0:
        return handle.isLatticeSelected();
      case 1:
        return handle.isSelected();
      case 2:
        return "";
      case 3:
        return "";
      default:
        assert false;
    }
    return null;
  }

  @Override
  public boolean isCellEditable(Object node, int column) {
    if (column > 1)
      return false;
    if (node instanceof DatasetHandle)
      return true;
    return false;
  }

  @Override
  public void setValueFor(Object node, int column, Object value) {
    if (!(node instanceof DatasetHandle))
      return;
    DatasetHandle handle = (DatasetHandle) node;
    if (column == 0) {
      handle.setLatticeSelected((Boolean) value);
    }
    if (column == 1) {
      handle.setSelected((Boolean) value);
    }

  }

}

ExclusiveBooleanEditor class (modified copy of DefaultCellRenderer)

public class ExclusiveBooleanEditor extends AbstractCellEditor implements TableCellEditor,
    TreeCellEditor {

  //
  // Instance Variables
  //

  /** The Swing component being edited. */
  protected JComponent editorComponent;
  /**
   * The delegate class which handles all methods sent from the <code>CellEditor</code>.
   */
  protected EditorDelegate delegate;
  /**
   * An integer specifying the number of clicks needed to start editing. Even if
   * <code>clickCountToStart</code> is defined as zero, it will not initiate until a click occurs.
   */
  protected int clickCountToStart = 1;

  //
  // Constructors
  //

  public ExclusiveBooleanEditor() {
    this(new JRadioButton());
    JRadioButton radioButton = (JRadioButton) getComponent();
    radioButton.setHorizontalAlignment(JRadioButton.CENTER);
  }

  public ExclusiveBooleanEditor(final JRadioButton radioButton) {
    editorComponent = radioButton;
    delegate = new EditorDelegate() {
      // FIXME replace
      @Override
      public void setValue(Object value) {
        boolean selected = false;
        if (value instanceof Boolean) {
          selected = ((Boolean) value).booleanValue();
        } else if (value instanceof String) {
          selected = value.equals("true");
        }
        radioButton.setSelected(selected);
      }

      @Override
      public Object getCellEditorValue() {
        return Boolean.valueOf(radioButton.isSelected());
      }
    };
    radioButton.addActionListener(delegate);
    radioButton.setRequestFocusEnabled(false);
  }

  /**
   * Returns a reference to the editor component.
   *
   * @return the editor <code>Component</code>
   */
  public Component getComponent() {
    return editorComponent;
  }

  //
  // Modifying
  //

  /**
   * Specifies the number of clicks needed to start editing.
   *
   * @param count an int specifying the number of clicks needed to start editing
   * @see #getClickCountToStart
   */
  public void setClickCountToStart(int count) {
    clickCountToStart = count;
  }

  /**
   * Returns the number of clicks needed to start editing.
   * 
   * @return the number of clicks needed to start editing
   */
  public int getClickCountToStart() {
    return clickCountToStart;
  }

  //
  // Override the implementations of the superclass, forwarding all methods
  // from the CellEditor interface to our delegate.
  //

  /**
   * Forwards the message from the <code>CellEditor</code> to the <code>delegate</code>.
   * 
   * @see EditorDelegate#getCellEditorValue
   */
  @Override
  public Object getCellEditorValue() {
    return delegate.getCellEditorValue();
  }

  /**
   * Forwards the message from the <code>CellEditor</code> to the <code>delegate</code>.
   * 
   * @see EditorDelegate#isCellEditable(EventObject)
   */
  @Override
  public boolean isCellEditable(EventObject anEvent) {
    return delegate.isCellEditable(anEvent);
  }

  /**
   * Forwards the message from the <code>CellEditor</code> to the <code>delegate</code>.
   * 
   * @see EditorDelegate#shouldSelectCell(EventObject)
   */
  @Override
  public boolean shouldSelectCell(EventObject anEvent) {
    return delegate.shouldSelectCell(anEvent);
  }

  /**
   * Forwards the message from the <code>CellEditor</code> to the <code>delegate</code>.
   * 
   * @see EditorDelegate#stopCellEditing
   */
  @Override
  public boolean stopCellEditing() {
    return delegate.stopCellEditing();
  }

  /**
   * Forwards the message from the <code>CellEditor</code> to the <code>delegate</code>.
   * 
   * @see EditorDelegate#cancelCellEditing
   */
  @Override
  public void cancelCellEditing() {
    delegate.cancelCellEditing();
  }

  //
  // Implementing the TreeCellEditor Interface
  //

  /** Implements the <code>TreeCellEditor</code> interface. */
  @Override
  public Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected,
      boolean expanded, boolean leaf, int row) {
    String stringValue = tree.convertValueToText(value, isSelected, expanded, leaf, row, false);

    delegate.setValue(stringValue);
    return editorComponent;
  }

  //
  // Implementing the CellEditor Interface
  //
  /** Implements the <code>TableCellEditor</code> interface. */
  @Override
  public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected,
      int row, int column) {
    delegate.setValue(value);
    if ((editorComponent instanceof JCheckBox) || (editorComponent instanceof JRadioButton)) {
      // in order to avoid a "flashing" effect when clicking a checkbox
      // in a table, it is important for the editor to have as a border
      // the same border that the renderer has, and have as the background
      // the same color as the renderer has. This is primarily only
      // needed for JCheckBox since this editor doesn't fill all the
      // visual space of the table cell, unlike a text field.
      TableCellRenderer renderer = table.getCellRenderer(row, column);
      Component c =
          renderer.getTableCellRendererComponent(table, value, isSelected, true, row, column);
      if (c != null) {
        editorComponent.setOpaque(true);
        editorComponent.setBackground(c.getBackground());
        if (c instanceof JComponent) {
          editorComponent.setBorder(((JComponent) c).getBorder());
        }
      } else {
        editorComponent.setOpaque(false);
      }
    }
    return editorComponent;
  }


  //
  // Protected EditorDelegate class
  //

  /**
   * The protected <code>EditorDelegate</code> class.
   */
  protected class EditorDelegate implements ActionListener, ItemListener, Serializable {

    /** The value of this cell. */
    protected Object value;

    /**
     * Returns the value of this cell.
     * 
     * @return the value of this cell
     */
    public Object getCellEditorValue() {
      return value;
    }

    /**
     * Sets the value of this cell.
     * 
     * @param value the new value of this cell
     */
    public void setValue(Object value) {
      this.value = value;
    }

    /**
     * Returns true if <code>anEvent</code> is <b>not</b> a <code>MouseEvent</code>. Otherwise, it
     * returns true if the necessary number of clicks have occurred, and returns false otherwise.
     *
     * @param anEvent the event
     * @return true if cell is ready for editing, false otherwise
     * @see #setClickCountToStart
     * @see #shouldSelectCell
     */
    public boolean isCellEditable(EventObject anEvent) {
      if (anEvent instanceof MouseEvent) {
        return ((MouseEvent) anEvent).getClickCount() >= clickCountToStart;
      }
      return true;
    }

    /**
     * Returns true to indicate that the editing cell may be selected.
     *
     * @param anEvent the event
     * @return true
     * @see #isCellEditable
     */
    public boolean shouldSelectCell(EventObject anEvent) {
      return true;
    }

    /**
     * Returns true to indicate that editing has begun.
     *
     * @param anEvent the event
     */
    public boolean startCellEditing(EventObject anEvent) {
      return true;
    }

    /**
     * Stops editing and returns true to indicate that editing has stopped. This method calls
     * <code>fireEditingStopped</code>.
     *
     * @return true
     */
    public boolean stopCellEditing() {
      fireEditingStopped();
      return true;
    }

    /**
     * Cancels editing. This method calls <code>fireEditingCanceled</code>.
     */
    public void cancelCellEditing() {
      fireEditingCanceled();
    }

    /**
     * When an action is performed, editing is ended.
     * 
     * @param e the action event
     * @see #stopCellEditing
     */
    @Override
    public void actionPerformed(ActionEvent e) {
      ExclusiveBooleanEditor.this.stopCellEditing();
    }

    /**
     * When an item's state changes, editing is ended.
     * 
     * @param e the action event
     * @see #stopCellEditing
     */
    @Override
    public void itemStateChanged(ItemEvent e) {
      ExclusiveBooleanEditor.this.stopCellEditing();
    }
  }

  public static class ExclusiveBooleanRenderer extends JRadioButton implements TableCellRenderer,
      UIResource {
    private static final Border noFocusBorder = new EmptyBorder(1, 1, 1, 1);
    private static final JLabel emptyLabel = new JLabel("");

    public ExclusiveBooleanRenderer() {
      super();
      setHorizontalAlignment(JRadioButton.CENTER);
      setBorderPainted(true);
    }

    @Override
    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
        boolean hasFocus, int row, int column) {

      // Don't draw if it is not changeable
      if (value == null) {
        if (isSelected) {
          emptyLabel.setForeground(table.getSelectionForeground());
          emptyLabel.setBackground(table.getSelectionBackground());
        } else {
          emptyLabel.setForeground(table.getForeground());
          emptyLabel.setBackground(table.getBackground());
        }

        return emptyLabel;
      }
      if (isSelected) {
        setForeground(table.getSelectionForeground());
        super.setBackground(table.getSelectionBackground());
      } else {
        setForeground(table.getForeground());
        setBackground(table.getBackground());
      }
      setSelected((value != null && ((Boolean) value).booleanValue()));

      if (hasFocus) {
        setBorder(UIManager.getBorder("Table.focusCellHighlightBorder"));
      } else {
        setBorder(noFocusBorder);
      }

      return this;
    }
  }

} // End of class JCellEditor

Upvotes: 1

Related Questions