The JSmartGridTMTutorial

How to Use the JSmartGrid

What packages should be imported?

The JSmartGrid API contains the following packages:

Examples

The rest of this section describes how to use the JSmartGrid to accomplish the following topics:

Creating a Simple Grid

A snapshot of SimpleGridDemo.
Try this:

  1. Compile and run SimpleGridDemo. The source file is SimpleGridDemo.java(in a .java source file).
  2. This is a simple example of a JSmartGrid to demonstrate its basic characteristics.
  3. SINGLE CELL OPERATION. The unit of selection for the JSmartGrid is the cell. To see how this functions, position the cursor over "22". Now press the mouse button and drag to the right and down over several rows and columns.  The top left cell has a different appearance because it is not only selected, but is editable and has the focus.
  4. MULTIPLE CELL OPERATION. Now click on the cell that contains "11". Then Ctrl-click on the one that contains "34". Then Ctrl-click on the one that contains "52".  (Any variation of cells may be selected.)  Now hit any key, for instance 'a'. The last cell selected, which has the focus, is the one that is edited.  You may also start editing any editable cell by double-clicking into it.
  5. SIZING BEHAVIOR. Now try resizing the grid.  Note that the columns adjust their size according to that of the grid, while the rows maintain their height, even if it is vertically resized.  This demonstrates the difference in default sizing behaviors for rows and columns, even if the set of possible behaviors for each is identical. If the grid is resized so that the total height is less than the sum of the height of  the rows, a vertical scrollbar will appear.
  6. HEADERS. You may notice that there are no headers in this example.  Headers are not included as a default in the JSmartGrid but can easily be added.  See below an example of a simple grid with headers.

Here is the code for creating the grid in SimpleGridDemo.java(in a .java source file):
  grid_ = new JSmartGrid();
  for (int col = 0; col < grid_.getColumnCount();  col++) {
    grid_.setColumnWidth(col,50);
    for (int row = 0; row < grid_.getRowCount();  row++)
      grid_.setValueAt("" + row + col, row, col);
  }
The SimpleGridDemo example uses the default JSmartGrid constructor. This creates an in-memory, editable GridModel with ten rows and ten columns. The actual class is GenericGridModel(in the API reference documentation)String data is stored in each cell, formed by appending the row and column indexes.

Adding a Grid to a Container

A snapshot of SimpleGridDemo.

To put a grid in a scroll pane, you need just the following line of code:

JScrollPane scrollPane = new JScrollPane(grid_);
The scroll pane's viewing area is the same as the grid's preferred viewing size. It will generate a vertical scroll bar if the grid is resized so that its height is smaller than the viewing area, as in the image above.

Setting Grid Properties

A snapshot of PropGridDemo.

The properties of the grid can be easily modified by adding the appropriate lines, as shown below:


  1. Compile and run PropGridDemo. The source file is PropGridDemo.java(in a .java source file).
  2. TO ADD HEADERS, add the following two lines:
  3.   grid_.setAutoCreateColumnHeader(true);
      grid_.setAutoCreateRowHeader(true);
    Now, as soon as the grid is put inside a JScrollPane,standard headers will be added automatically . Note that when the user scrolls down, the column headers remain visible at the top of the viewing area.
     
  4. TO ADJUST THE TEXT FORMAT, add the following line:
  5.   grid_.setFont(new Font("Dialog", 0, 22));
  6. TO ADJUST THE COLOR OF THE GRID (the separator lines), for instance to blue, add the following line:
  7.   grid_.setGridColor(Color.blue);
  8. TO ADJUST THE BACKGROUND COLOR OF A CELL, for instance to green, add the following line:
  9.   grid_.setFocusBackgroundColor(Color.green);
  10. TO ADJUST THE SELECTION MODEL (for instance, to select by rows), add the following line:
  11.   grid_.setSelectionMode(JSmartGrid.MULTIPLE_ROW_SELECTION);
    This property is actually the combination of two properties, the selection unit, and the selection policy, so the line above could also have been written as follows:
      grid_.setSelectionUnit(JSmartGrid.UNIT_ROW);
      grid_.setSelectionPolicy(JSmartGrid.POLICY_MULTIRANGE);

Displaying Data from a Database

A snapshot of DBSnapshotGridDemo.

The data displayed in the former examples had been created to display data stored in memory. Now we'll retrieve data from a database.


  1. To run this example, you will need to have a database accessible through JDBC or you can install our example database: How to install example database.
  2. Compile and run DBSnapshotGridDemo. The source files are DBSnapshotGridDemo.java(in a .java source file), and DBSnapshotGridModel.java(in a .java source file).
  3. Even though there are no headers yet, the cells' widths were created to be interactively resizable by adding the line:
  4.   grid_.setResizable(JSmartGrid.HORIZONTAL,true);
  5. When cells are resizable along a certain orientation (here, horizontally), they can be automatically resized to fit their content by double-clicking on the appropriate border (left border for horizontal orientation, bottom border for vertical) while the resizing cursor is visible. Give it a try.

The example extends the GenericGridModel(in the API reference documentation) class to fill it with the results of a database query. It is called DBSnapshotGridModel to emphasize the fact that the model only contains an in-memory copy of the database data. The model is filled by the following code that ends up calling the setDataVector(in the API reference documentation) method, and is the main part of the setQuery method.
  try {
    ResultSet resultSet = statement_.executeQuery(query);
    ResultSetMetaData metaData = resultSet.getMetaData();

    Vector rows = new Vector();
    int colCount = metaData.getColumnCount();
    // Get all rows.
    while (resultSet.next()) {
      Vector newRow = new Vector(colCount);
      for (int i = 1; i <= colCount; i++)
        newRow.add(resultSet.getObject(i));
      rows.add(newRow);
    }
    setDataVector(rows);
  } catch (SQLException ex) {
    System.err.println(ex);
  }

The model is then instantiated, and transmitted to the grid:
  DBSnapshotGridModel model = new DBSnapshotGridModel("jdbc:odbc:base", "sun.jdbc.odbc.JdbcOdbcDriver");
  model.setQuery("SELECT * FROM DemoTable");
  grid_ = new JSmartGrid(model);

Displaying Data from a Database with Headers

A snapshot of HeaderGridDemo.

Now that you know how to add standard automatic headers, we'll see how to create headers with specific column names. This example builds a second grid model by adding the column header component to the GridModel in the code of the previous example.  The source code is in HeaderGridDemo.java(in a .java source file) and HeaderGridModel.java(in a .java source file). The model is given a new instance variable:

  private AbstractGridModel columnHeaderModel_;
and the code in setQuery becomes:
  ResultSet resultSet = statement_.executeQuery(query);
  ResultSetMetaData metaData = resultSet.getMetaData();
  final int colCount = metaData.getColumnCount();

  final String[] colNames = new String[colCount];
  for (int i = 0; i < colCount; i++)
    colNames[i] = metaData.getColumnName(i+1);

  columnHeaderModel_ = new AbstractGridModel () {
    public int getRowCount() {
      return 1;
    }
    public int getColumnCount() {
      return colCount;
    }
    public Object getValueAt(int row, int column) {
      return colNames[column];
    }
  };

  Vector rows = new Vector();
  while (resultSet.next()) {
    Vector newRow = new Vector(colCount);
    for (int i = 0; i < colCount; i++)
      newRow.add(resultSet.getObject(i+1));
    rows.add(newRow);
  }
  setDataVector(rows);
The new code appears in bold. It defines a simple model with only one row, the same number of columns as the original model, and the column names as data to be displayed. It builds on the AbstractGridModel(in the API reference documentation) which, by default, defines a read-only model.

 Now that we have a header model, how do we use it? The column header is explicitly created and attached in the code that stores the grid inside the JScrollPane:

  grid_.setAutoCreateRowHeader(true);
  JScrollPane js = new JScrollPane(grid_);
  grid_.setColumnHeader(new JSmartGridHeader(grid_,JSmartGrid.HORIZONTAL,model.getColumnHeaderModel(),null,null));
As described in SizeGridDemo.java(in a .java source file) and SizeGridModel.java(in a .java source file), sizes can be set for columns or rows using the preferred, minimal and maximum ruler models.

Displaying a TableModel

Now we'll show how to display a TableModel in a JSmartGrid

A snapshot of TableToGridDemo.

The source code is in TableToGridDemo.java(in a .java source file) and TableToGridMapper.java(in a .java source file). We'll now implement a GridModel which works like a gateway, with:

The GridModel must both translate its requests from the JSmartGrid into requests to the TableModel, then translate notifications from the TableXXXModels into GridModel notifications.
public class TableToGridMapper extends AbstractGridModel
                        implements TableModelListener, TableColumnModelListener {
  private TableModel source_;
  private TableColumnModel columns_;
  private GridModel columnHeaderModel_;
  ...
}
The initial connection to the table model is established by the following code:
  public void setTableModel(TableModel source) {
    if (source!=source_) {
      if (source_!=null)
        source_.removeTableModelListener(this);
      setTableColumnModel(null);
      source_=source;
      if (source_==null)
        columnHeaderModel_=null;
      else {
        source_.addTableModelListener(this);
        columnHeaderModel_=new AbstractGridModel() {
          public int getRowCount() {
            return 1;
          }
          public int getColumnCount() {
            return source_.getColumnCount();
          }
          public Object getValueAt(int row, int column) {
            return source_.getColumnName(column);
          }
        };
      }
      fireGridModelChanged();
    }
  }
This registers the GridModel as a TableModelListener (after removing the existing model and listener, if necessary) and builds the secondary model for the column headers, which will be made available by the following method:
  public GridModel getColumnHeaderModel() {
    return columnHeaderModel_;
  }
The request translation is done by the following code:
  public int getRowCount() {
    return source_==null?0:source_.getRowCount();
  }
  public int getColumnCount() {
    return source_==null?0:source_.getColumnCount();
  }
  public boolean isCellEditable(int row, int column) {
    return source_!=null && source_.isCellEditable(row, column);
  }
  public Object getValueAt(int row, int column) {
    return source_==null?null:source_.getValueAt(row,column);
  }
  public void setValueAt(Object value,int row, int column) {
    if (source_!=null)
      source_.setValueAt(value,row,column);
  }
Note that the code is mainly delegation. The notification is done by the following method, part of the TableModelListener API:
  public void tableChanged(TableModelEvent e) {
    if (e==null) {
      fireGridModelChanged();
      return;
    }
    int sourceColumn=e.getColumn();
    int firstSourceRow=e.getFirstRow();
    int lastSourceRow=e.getLastRow();
    boolean whole=firstSourceRow==TableModelEvent.HEADER_ROW;
    boolean byRow=sourceColumn==TableModelEvent.ALL_COLUMNS;
    if (whole)
      fireGridModelChanged();
    else {
      int rowCount=lastSourceRow-firstSourceRow;
      if (rowCount!=Integer.MAX_VALUE)
        rowCount++;
      switch(e.getType()) {
      case TableModelEvent.UPDATE:
        if (byRow)
          fireGridRowsChanged(firstSourceRow,rowCount);
        else
          fireGridCellsChanged(firstSourceRow,sourceColumn,rowCount,1);
        break;
      case TableModelEvent.INSERT:
        fireGridRowsInserted(firstSourceRow,rowCount);
        break;
      case TableModelEvent.DELETE:
        fireGridRowsDeleted(firstSourceRow,rowCount);
        break;
      }
    }
  }
The code in red handles extreme cases that may be generated by TableModels and must not be neglected.

The TableColumnModel, if any, is connected through:

  public void setTableColumnModel(TableColumnModel columns) {
    if (columns!=columns_) {
      if (columns_!=null)
        columns_.removeColumnModelListener(this);
      columns_=columns;
      if (columns_!=null) {
        columns_.addColumnModelListener(this);
        columnHeaderModel_=new AbstractGridModel() {
          public int getRowCount() {
            return 1;
          }
          public int getColumnCount() {
            return columns_.getColumnCount();
          }
          public Object getValueAt(int row, int column) {
            return columns_.getColumn(column).getHeaderValue();
          }
        };
        fireGridModelChanged();
      }
    }
  }
Note that if a TableColumnModel is specified, it overrides the column header model issued from the TableModel. The TableColumnModel-related notification translation is done in the following methods, as part of the TableColumnListener API:
  public void columnAdded(TableColumnModelEvent e) {
    fireGridColumnsInserted(e.getToIndex(),1);
  }
  public void columnRemoved(TableColumnModelEvent e) {
    fireGridColumnsDeleted(e.getFromIndex(),1);
  }
  public void columnMoved(TableColumnModelEvent e) {
    fireGridModelChanged();
  }
All of this is put together in the main code. First, table models are created:
  TableModel tableModel = new DefaultTableModel(10,10);
  for (int row = 0; row < 10;  row++) {
    for (int col = 0; col < 9;  col++)
      if ((col%2)==(row%2))
        tableModel.setValueAt("" + row + col, row, col);
      else
        tableModel.setValueAt(new Integer(row*10 + col), row, col);
    tableModel.setValueAt(new Boolean((row%2)==0), row, 9);
  }
  TableColumnModel tableColumnModel = new DefaultTableColumnModel();
  for (int col = 0; col < 10;  col++) {
    TableColumn tc = new TableColumn(col);
    String num;
    switch (col) {
      case 0: num="st";break;
      case 1: num="nd";break;
      case 2: num="rd";break;
      default: nu="th";break;
    }
    tc.setHeaderValue((col+1)+num);
    tableColumnModel.addColumn(tc);
  }
Then the grid is built on them:
  TableToGridMapper tableToGridMapper = new TableToGridMapper(tableModel);
  tableToGridMapper.setTableColumnModel(tableColumnModel);
  grid_ = new JSmartGrid(tableToGridMapper);
  JScrollPane js = new JScrollPane(grid_);
  for (int col = 0; col < grid_.getColumnCount();  col++)
    grid_.setColumnWidth(col, 50);
  grid_.setColumnHeader(new JSmartGridHeader(grid_,grid_.HORIZONTAL,tableToGridMapper.getColumnHeaderModel(),null,null));    
  getContentPane().add(js, BorderLayout.CENTER);

Basic Customizing for Drawing Properties

Up to now we have used the default mechanisms to render the data in individual cells. This mechanism is based on the class of the data. Now we are going to vary rendering according to the value itself, and show numeric values in red whenever they are negative.

A snapshot of StyleGridDemo.

The source code is in StyleGridDemo.java(in a .java source file). To do this we don't need to overload the DefaultStyleModel(in the API reference documentation) class, we can just modify the renderer attached to numeric values by extending the DefaultStyleModel.NumberRenderer(in the API reference documentation) class:

  DefaultStyleModel custStyleModel = new DefaultStyleModel();
  custStyleModel.setRenderer(
    Integer.class,
    new DefaultStyleModel.NumberRenderer() {
      public Component getComponent(Object value, boolean isSelected, boolean hasFocus, boolean isEditable, int row, int column, GridContext context) {
        Component c = super.getComponent(value, isSelected, hasFocus, isEditable, row, column, context);
        if (value instanceof Integer && ((Integer)value).intValue() < 0)
          c.setForeground(Color.red);
        return c;
      }
    }
  );
Note that since the getComponent method passes the row and column as arguments, it might easily restrict this effect to particular cells (for instance cells in a particular column). The appearance of the screenshot is then obtained by:
  for (int col = 0; col < grid_.getColumnCount();  col++) {
    grid_.setColumnWidth(col, 50);
    for (int row = 0; row < grid_.getRowCount();  row++) {
      int k=row*grid_.getColumnCount()+col;
      if ((col+row)%2 != 0)
        k=-k;
      grid_.setValueAt(new Integer(k), row, col);
    }
  }
  grid_.setStyleModel(custStyleModel);

Customizing Advanced Renderers and Editors

Now w'll demonstrate how to implement a "choose color" cell.

A snapshot of ComboGridDemo, which displays a grid with spanned rows.

The source is in WComboGridDemo.java(in a .java source file). The color is rendered as its name with a background of the actual color, and is edited using a combo-box. First, we define a class to represent the data:

  class NamedColor extends Color {
    String name;
    public NamedColor(Color color, String name) {
      super(color.getRGB());
      this.name = name;
    }
    
    public Color getTextColor() {
      int r = getRed();
      int g = getGreen();
      int b = getBlue();
      if (r > 240 || g > 240)
        return Color.black;
      else
        return Color.white;
    }
    public String toString() {
      return name;
    }
  }
Then, we define a renderer class:
  class NamedColorCellRenderer extends DefaultGridCellRenderer {
    public NamedColorCellRenderer() {
      setHorizontalAlignment(JLabel.CENTER);
    }
    public void setValue(Object value) {
      NamedColor c = (NamedColor) value;
      setBackground(c);
      setForeground(c.getTextColor());
      setText(c.toString());
    }
  }
Next, we attach it to the DefaultStyleModel thus:
  DefaultStyleModel styleModel = new DefaultStyleModel();
  styleModel.setRenderer(NamedColor.class, new NamedColorCellRenderer());
After filling a combo with the color names, we attach an editor:
  styleModel.setEditor(NamedColor.class, new DefaultGridCellEditor(comboBox));
Finally, we need to tell the grid to use this modified style:
  grid_.setStyleModel(styleModel);
This example also demonstrates the use of images (here in the second column).  To define a model where the cells in the second column will never be edited, add the following lines:
  grid_ = new JSmartGrid(new GenericGridModel(maxRow,10) {
    public boolean isCellEditable(int row,int column) {
      return column != 1;
    }
  });
.

Defining Simple Spans

Now we are going to explore the spanning possibilities, i.e. the possibility of merging several cells in a rectangular area.

A snapshot of DirectSpanGridDemo, which displays a grid with spanned rows.

First we show how to span cells to form regular geometrical patterns. The source code is in DirectSpanGridDemo.java(in a .java source file). We start from the SimpleGridDemo with headers added, and after the grid creation, we add:

  grid_.setSpanModel(new AbstractDirectSpanModel() {
    public ExtentCell getSpanOver(final int row, final int column) {
      if ((row%3)!=0 && (column%3)!=0)
        return new ExtentCell() {
          public int getRow() {
            return row-(row%3)+1;
          }
          public int getRowCount() {
            return 2;
          }
          public int getColumn() {
            return column-(column%3)+1;
          }
          public int getColumnCount() {
            return 2;
          }
          public Object getIdentifier() {
            return null;
          }
        };
      else
        return null;
    }
    public boolean isEmpty() {
      return false;
    }
  });

Defining Spans from Structure

Adding on to the previous example, we group the records by the value of the first column ("ProductID"), showing the other columns as data and displaying the grouping value across the full width of the grid at the start of each group, as in the screenshot below:

A snapshot of QuerySpanGridDemo, which displays a grid with spanned rows.

To accomplish this, the grid model of the HeaderGridDemo example will be rewritten. The source code is in QuerySpanGridDemo.java(in a .java source file) and QuerySpanGridModel.java(in a .java source file). First we introduce a new boolean instance variable in order to switch between normal and spanned modes:

  private boolean spanned_;
Some of the methods of GridModel will be redefined.
  public int getColumnCount() {
    if (spanned_)
      return  super.getColumnCount() - 1;
    return  super.getColumnCount();
  }
  
  public Object getValueAt(int row, int column) {
    if (spanned_)
      column++;
    return super.getValueAt(row, column);
  }
The changes to the base implementation appear in bold. The effect is that the first column is hidden when spanning is turned on. Since this column must also be hidden in the column header model, the setQuery method must also be rewritten :
  ResultSet resultSet = statement_.executeQuery(query);
  ResultSetMetaData metaData = resultSet.getMetaData();
  final int colCount = metaData.getColumnCount();

  final String[] colNames = new String[colCount];
  for (int i = 0; i < colCount; i++)
    colNames[i] = metaData.getColumnName(i+1);

  columnHeaderModel_ = new AbstractGridModel () {
    public int getRowCount() {
      return 1;
    }
    public int getColumnCount() {
      if (spanned_)
        return colCount -  1;
      return colCount;
    }
    public Object getValueAt(int row, int column) {
      if (spanned_)
        column++;
      return colNames[column];
    }
  };
  spanned_=false;
  Vector rows = new Vector();
  while (resultSet.next()) {
    Vector newRow = new Vector(colCount);
    for (int i = 0; i < colCount; i++)
      newRow.add(resultSet.getObject(i+1));
    rows.add(newRow);
  }
  setDataVector(rows);
Moreover, "Group Header" rows must be added or removed as spanning is turned on and off. This can be done as follow:
  private TreeSet spannedRows_ = new TreeSet();
  public void setSpanned(boolean spanned) {
    if (spanned_ != spanned) {
      String label=(String)columnHeaderModel_.getValueAt(0,0);
      spanned_ = spanned;
      if (spanned) {
        Object spanValue = null;
        for (int row = 0; row < getRowCount(); row++) {
          Object curValue = super.getValueAt(row, 0);
          if (spanValue!=null && !spanValue.equals(curValue) ||
              spanValue==null && curValue!=null) {
            spanValue = curValue;
            Vector newRow = new Vector();
            for (int j=0;j < super.getColumnCount();j++)
              newRow.add(label+": "+curValue);
            insertRow (row, newRow);
            spannedRows_.add(new Integer(row++));
          }
        }
      } else {
        int deltaRow = 0;
        for (Iterator i = spannedRows_.iterator(); i.hasNext(); ) {
          Integer row = (Integer)i.next();
          removeRows(row.intValue()-deltaRow++, 1);
        }
        spannedRows_.clear();
      }
      fireGridModelChanged();
    }
  }


Now we link the above data structure to an efficient SpanModel.

  private class SpannedViewSpanModel extends AbstractDirectSpanModel {
    public ExtentCell getSpanOver(final int row, int column) {
      if (!spanned_ || !spannedRows_.contains(new Integer(row)))
        return null;
      return new ExtentCell() {
        public int getRow() {
          return row;
        }
        public int getRowCount() {
          return 1;
        }
        public int getColumn() {
          return 0;
        }
        public int getColumnCount() {
          return QuerySpanGridModel.this.getColumnCount();
        }
        public Object getIdentifier() {
          return null;
        }
      };
    }
    
    public boolean isEmpty() {
      return !spanned_ || spannedRows_.isEmpty();
    }
  }
This SpanModel instance will be made available to the QuerySpanGridDemo.
  private AbstractSpanModel spanModel_=new SpannedViewSpanModel();
  public SpanModel getSpanModel() {
    return spanModel_;
  }
The QuerySpanGridDemo will keep a reference to the model as an instance variable:
  private QuerySpanGridModel model_;
To create the grid, we use the folowwing:
  model_ = new QuerySpanGridModel("jdbc:odbc:base", "sun.jdbc.odbc.JdbcOdbcDriver");
  model_.setQuery("SELECT * FROM DemoTable ORDER BY ProductID");
  
  grid_ = new JSmartGrid(model_,model_.getSpanModel());
  JScrollPane js = new JScrollPane(grid_);
  grid_.setColumnHeader(new JSmartGridHeader(grid_,grid_.HORIZONTAL,model_.getColumnHeaderModel(),null,null));
  getContentPane().add(js, BorderLayout.CENTER);
The changes, again appearing in bold, are: To activate and deactivate the spanning, we need a menu item:
  private JCheckBoxMenuItem spanMenuItem = new JCheckBoxMenuItem("Span");
and the item then triggers the right method:
  spanMenuItem.addActionListener(
    new ActionListener () {
      public void actionPerformed (ActionEvent evt) {
        model_.setSpanned(spanMenuItem.getState());
      }
    }
  );

Free-form Spans

The spans defined in the previous example were structural, in the sense that they were strongly connected to the data. Now we'll build a simple application that will allow the user to specify spans at will.

A snapshot of CollectionSpanGridDemo.

The source code is in CollectionSpanGridDemo.java(in a .java source file). We'll start from the SimpleGridDemo with headers already added.

First, we introduce a new instance variable that will hold a reference to a GenericCollectionSpanModel(in the API reference documentation) object.

  private GenericCollectionSpanModel spanModel_ = new GenericCollectionSpanModel();
Then we add the following utility method to span or unspan the currently selected cells (we assume the selection mode is SINGLE_RECTANGLE_SELECTION(in the API reference documentation)):
  private void doSpan(boolean on) {
    GridSelectionModel gsm = grid_.getSelectionModel();
    if (gsm.isSelectionEmpty())
      return;
    int firstRow=gsm.getFirstSelectedRow();
    int rowCount=gsm.getLastSelectedRow() - firstRow + 1;
    int firstColumn=gsm.getFirstSelectedColumn();
    int columnCount=gsm.getLastSelectedColumn() - firstColumn + 1;

    Iterator spans=grid_.spanIterator(firstRow, firstColumn, rowCount, columnCount, false);
    while (spans.hasNext()) {
      ExtentCell span=(ExtentCell)spans.next();
      spanModel_.removeSpan(span.getRow(),span.getColumn());
    }

    if (on)
      spanModel_.addSpan(firstRow, firstColumn, rowCount, columnCount);
  }
The code in blue is necessary to remove any pre-existing spans in the selected area. We then create a new span if the "on" parameter is true. Since the mode is SINGLE_RECTANGLE_SELECTION, the selection bounds are identical to the selected area, and the GridSelectionModel specifications guarantee that any span intersecting this area will be fully included in it. This method will be connected with two menu items:
  JMenuItem spanOnMenuItem = new JMenuItem("Span On");
  spanOnMenuItem.addActionListener(
    new ActionListener () {
      public void actionPerformed (ActionEvent evt) {
        doSpan(true);
      }
    }
  );
  JMenuItem spanOffMenuItem = new JMenuItem("Span Off");
  spanOffMenuItem.addActionListener(
    new ActionListener () {
      public void actionPerformed (ActionEvent evt) {
        doSpan(false);
      }
    }
  );
The grid itself must be initialized in the following way:
  grid_ = new JSmartGrid();
  grid_.setAutoCreateColumnHeader(true);
  grid_.setAutoCreateRowHeader(true);
  grid_.setColumnAutoResizeMode(JSmartGrid.AUTO_RESIZE_OFF);
  grid_.setSpanModel(spanModel_);
  for (int col = 0; col < grid_.getColumnCount();  col++) {
    grid_.setColumnWidth(col, 50);
    for (int row = 0; row < grid_.getRowCount();  row++)
      grid_.setValueAt("" + row + col, row, col);
  }
  grid_.setSelectionMode(GridSelectionModel.SINGLE_RECTANGLE_SELECTION);
  grid_.setSpanModel(spanModel_);
  grid_.getModel().addGridModelListener(spanModel_);
The last statement, in blue, is necessary so that the GenericCollectionSpanModel will be notified whenever some structural change occurs in the GridModel.

Progressive Dragging

Now we'll demonstrate how to allow the user to move data by dragging header cells, using the setDraggingXXX of the API.  In this example, we demonstrate how to move a column, but rows can also be moved in the same way.

A snapshot of ColumnDragGridDemo.

In the example above, we drag the third column to the 5th position. One the mouse press is released, columns 4 and 5 have already been shifted one position to the left. The source code is in ColumnDragGridDemo.java(in a .java source file) and ColumnDragController.java(in a .java source file)).

We first define a controller, a class that implements the GridListener(in the API reference documentation) interface.

public class ColumnDragController extends GridAdapter {
  private int pos_;
  ...
}
The drag starts when the controller intercepts the mouse pressed event:
  public void gridMousePressed(GridEvent e) {
    JSmartGrid grid=(JSmartGrid)e.getSource();
    if (grid.isDraggable(JSmartGrid.HORIZONTAL)) {
      grid.setFirstDraggingItem(JSmartGrid.HORIZONTAL,e.getColumn());
      pos_=((MouseEvent)e.getSourceEvent()).getX();
    }
  }
The mouse dragged event follows:
  public void gridMouseDragged(GridEvent e) {
    JSmartGrid grid=(JSmartGrid)e.getSource();
    if (grid.isDragging(JSmartGrid.HORIZONTAL)) {
      int newpos=((MouseEvent)e.getSourceEvent()).getX();
      grid.setDraggingDistance(JSmartGrid.HORIZONTAL,newpos-pos_);
      int delta=grid.getDraggingDistance(JSmartGrid.HORIZONTAL);
      pos_=newpos-delta;
    }
  }
And then, the mouse released event:
  public void gridMouseReleased(GridEvent e) {
    JSmartGrid grid=(JSmartGrid)e.getSource();
    if (grid.isDragging(JSmartGrid.HORIZONTAL))
      grid.resetDragging(JSmartGrid.HORIZONTAL);
  }
The key statement is the one in red. This calls to setDraggingDistance(in the API reference documentation) which both redisplays the "shifted" cells and fires ruler model events. The GenericGridModel(in the API reference documentation) used by default by the JSmartGrid is ready to handle these events, which means that, in this case, it is a RulerModelListener(in the API reference documentation) and can handle RULER_ITEMS_MOVED(in the API reference documentation). The following code is added to the ColumnDragGridDemo to connect the grid and the controller:
  public void attachController() {
    GenericGridModel model=(GenericGridModel)grid_.getModel();
    RulerModel rm=grid_.getColumnModel();
    rm.addRulerModelListener(model);
    JSmartGrid header=(JSmartGrid)grid_.getColumnHeader();
    header.setDraggable(JSmartGrid.HORIZONTAL,true);
    header.addGridListener(new ColumnDragController());
  }
The first three lines ensure the generic grid model will be notified of moves as they happen.  The second to last line allows the user to drag the cells in the column header, and the last line connects the controller to the header.

Now compile and run the example, and try dragging some cell back and forth. Note that the column headers themselves do not move since the default model for headers is just a spreadsheet-style position-marker . The GenericGridModel contains special handling code (the rulerStructureChanged method) to keep the cells from snapping back into place when dragging is over.

Note: It is also possible to drag several items at once, by using setLastDraggingItem(in the API reference documentation).

Direct Dragging

In the previous example, columns were shifted as soon as the one being dragged was placed before them. Now we'll show how to drag a cell, this time waiting for the final mouse release before moving the surrounding cells .

A snapshot of DirectColumnDragGridDemo.

Note that this time, the columns from four to six have not moved yet. This is made possible by a "veto" API. Before the setDraggingDistance(in the API reference documentation) method fires a RulerModelEvent.RULER_ITEMS_MOVED(in the API reference documentation), it modifies a VetoableProperty whose name is columnDragging or rowDragging according to the orientation. If any registered listener raises a PropertyVetoException, the moving event is never fired (the source code is in DirectColumnDragGridDemo.java(in a .java source file) and DirectColumnDragController.java(in a .java source file)). The controller class becomes:

public class DirectColumnDragController extends GridAdapter implements VetoableChangeListener {
  private int pos_;
  ...
}
When the mouse is pressed, it registers as VetoableChangeListener:
  public void gridMousePressed(GridEvent e) {
    JSmartGrid grid=(JSmartGrid)e.getSource();
    if (grid.isDraggable(JSmartGrid.HORIZONTAL)) {
      grid.setFirstDraggingItem(JSmartGrid.HORIZONTAL,e.getColumn());
      pos_=((MouseEvent)e.getSourceEvent()).getX();
      grid.addVetoableChangeListener("columnDragging",this);
    }
  }
The mouse drag handling method is not modified directly, so we add the following veto:
  public void vetoableChange(PropertyChangeEvent e) throws PropertyVetoException {
    if ("columnDragging".equals(e.getPropertyName()))
      throw new PropertyVetoException("",e);
  }
On the mouse release, the controller must both unregister itself as a VetoableChangeListener and make a last, non-vetoed call to setDraggingDistance(in the API reference documentation).
  public void gridMouseReleased(GridEvent e) {
    JSmartGrid grid=(JSmartGrid)e.getSource();
    if (grid.isDragging(JSmartGrid.HORIZONTAL)) {
      grid.removeVetoableChangeListener("columnDragging",this);
      int newpos=((MouseEvent)e.getSourceEvent()).getX();
      grid.setDraggingDistance(JSmartGrid.HORIZONTAL,newpos-pos_);
      grid.resetDragging(JSmartGrid.HORIZONTAL);
    }
  }

Data Transfer

Now we are going to do a data transfer by creating a link between our grid and the data we want to display. We'll start with the previous grid, which had one span defined:

A snapshot of CopyPasteGridDemo.

Now we open a spreadsheet application, with an area of nine cells, filled and selected:

A snapshot of StarOffice.

Now if we copy this selection into the clipboard, selecting in our grid the cells from <C, 3> to <E, 5>, and paste from the clipboard, we get:

A snapshot of CopyPasteGridDemo.

The source code is in ClipboardGridDemo.java(in a .java source file)) but first we will need to use a particular package:

import java.awt.datatransfer.*;
We then define instance variables to identify the area of interest:
  private int lastRow_;
  private int firstRow_;
  private int lastColumn_;
  private int firstColumn_;
Next, we define a new method:
  private void paste(Transferable data) {
    try {
      Reader reader=DataFlavor.stringFlavor.getReaderForText(data);
      BufferedReader buffer=new BufferedReader(reader);
      String line=null;
      int row=firstRow_;
      // If the clipboard has too few lines or columns, erase cells 
      boolean recordsOver=false;
      while (row <= lastRow_) {
        if (!recordsOver) {
          line=buffer.readLine();
          if (line==null) {
            recordsOver=true;
            line="";
          }
        }
        StringTokenizer toker=new StringTokenizer(line,"\t",true);
        int column=firstColumn_;
        while (toker.hasMoreElements()) {
          String tok=toker.nextToken();
          boolean over=false;
          if ("\t".equals(tok))
            tok="";
          else if (toker.hasMoreElements())
            toker.nextToken(); // should be "\t"!
          else
            over=true;
          ExtentCell span=grid_.getSpanOver(row,column);
          if (span==null || span.getRow()==row && span.getColumn()==column)
            grid_.setValueAt(tok,row,column);
          if (over || ++column > lastColumn_)
            break;
        }
        if (++row > lastRow_)
          break;
      }
    } catch(Exception ex) {
      ex.printStackTrace();
    }
  }
This code makes the assumption that the data from the clipboard will be in plain text flavor. It also assumes it will be structured with tabs (as field separators) and newlines (as record separators), which is the way that most spreadsheets export data. The "fields" are then stored in the corresponding cells, avoiding any cell covered by spans. We then connect to a popup menu item, as follows:
  JPopupMenu popup = new JPopupMenu();
  JMenuItem paste = new JMenuItem("Paste");
  paste.addActionListener(
    new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        Transferable data=clipboard_.getContents(ClipboardGridDemo.this);
        paste(data);
      }
    }
  );
The popup itself is connected to the grid and is responsible for storing valid values into the firstRow_... instance variables:
  grid_.addGridListener(
    new GridAdapter() {
      public void gridMouseReleased(GridEvent e) {
        if (e.getSourceEvent()!=null && ((MouseEvent)e.getSourceEvent()).isPopupTrigger()) {
          JSmartGrid grid=(JSmartGrid)e.getSource();
          int column = e.getColumn();
          int row = e.getRow();
          if (column >= 0 && row > =0) {
            int x=((MouseEvent)e.getSourceEvent()).getX();
            int y=((MouseEvent)e.getSourceEvent()).getY();
            GridSelectionModel sm=grid.getSelectionModel();
            lastRow_=sm.getLastSelectedRow();
            firstRow_=sm.getFirstSelectedRow();
            lastColumn_=sm.getLastSelectedColumn();
            firstColumn_=sm.getFirstSelectedColumn();
            if (grid_.isSelectionEmpty() ||
                row < firstRow_ || row > lastRow_ ||
                column < firstColumn_ || column > lastColumn_) {
              firstRow_=lastRow_=row;
              firstColumn_=lastColumn_=column;
            }
            popup.show(grid, x, y);
            paste.setEnabled(clipboard_.getContents(this)!=null);
          }
        }
      }
    }
  );
Transferring data the opposite way is just as easy. We define a new method:
  private Transferable copy() {
    String fmt=""; 
    for (int row=firstRow_;row <= lastRow_;row++) {
      if (row > firstRow_)
        fmt+="\n";
      for (int column=firstColumn_;column <= lastColumn_;column++) {
        if (column > firstColumn_)
          fmt+="\t";
        ExtentCell span=grid_.getSpanOver(row,column);
        if (span!=null && (span.getRow() !=row || span.getColumn() != column))
          continue;
        fmt+=grid_.getValueAt(row,column);
      }
    }
    return new StringSelection(fmt);
  }
Next, we add a new popup menu item:
  final JMenuItem copy = new JMenuItem("Copy");
  copy.addActionListener(
    new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        Transferable data=copy();
        clipboard_.setContents(copy,ClipboardGridDemo.this);
      }
    }
  );
  popup.add(copy);
The grid itself is built exactly like in the free-form spans example.
 

Now you are familiar with the basic functions fo the JSmartGrid Version 1.0.  We want you to share your imagination with us by sending us your questions and comments info@eliad.com.  We hope you enjoy the JSmartGrid and look forward to hearing from you soon!