The JSmartGrid API contains the following packages:
- com.eliad.swing is the main package and is where the JSmartGrid class resides. It also contains:
- the JSmartGridHeader class, a version of the JSmartGrid used as a header;
- GridEvent, GridListener, and GridAdapter that allow InputEvents to be handled in a component-oriented way;
- GridEditingEvent and GridEditingListener that allow editing events to be handled in the cells.
- com.eliad.model contains the definition of all models as interfaces, the corresponding events classes and listener interfaces, and abstract versions of the models that provide listener handling. It also contains generic versions of the GridModel and the CollectionSpanModel. The GenericGridModel is used by the default JSmartGrid constructor.
- com.eliad.model.defaults contains the definition of commonly used default versions for the DefaultRulerModel and the DefaultStyleModel. Default renderers and editors are included with the DefaultStyleModel.
- com.eliad.util contains some utility classes. The Intervals interface may be used when specifying particular size sequences for rows or columns. The SizeSequenceIntervals provides a default implementation based on the SizeSequence of Java Foundation Classes API/Swing technology.
The rest of this section describes how to use the JSmartGrid to accomplish the following topics:
- Creating a Simple Grid
- Adding a Grid to a Container
- Setting Grid Properties
- Displaying Data from a Database
- Displaying Data from a Database With Headers
- Displaying a TableModel
- Basic Customizing for Drawing Properties
- Customizing Advanced Renderers and Editors
- Defining Simple Spans
- Defining Spans Based on Structure
- Free-form Spans
- Progressive Dragging
- Direct Dragging
- Data Transfer
Try this: Here is the code for creating the grid in SimpleGridDemo.java:
- Compile and run SimpleGridDemo. The source file is SimpleGridDemo.java.
- This is a simple example of a JSmartGrid to demonstrate its basic characteristics.
- 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.
- 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.
- 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.
- 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.
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 . String data is stored in each cell, formed by appending the row and column indexes.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); }
To put a grid in a scroll pane, you need just the following line of code:
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.JScrollPane scrollPane = new JScrollPane(grid_);
The properties of the grid can be easily modified by adding the appropriate lines, as shown below:
- Compile and run PropGridDemo. The source file is PropGridDemo.java.
- TO ADD HEADERS, add the following two lines:
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.grid_.setAutoCreateColumnHeader(true); grid_.setAutoCreateRowHeader(true);
- TO ADJUST THE TEXT FORMAT, add the following line:
grid_.setFont(new Font("Dialog", 0, 22));- TO ADJUST THE COLOR OF THE GRID (the separator lines), for instance to blue, add the following line:
grid_.setGridColor(Color.blue);- TO ADJUST THE BACKGROUND COLOR OF A CELL, for instance to green, add the following line:
grid_.setFocusBackgroundColor(Color.green);- TO ADJUST THE SELECTION MODEL (for instance, to select by rows), add the following line:
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_.setSelectionMode(JSmartGrid.MULTIPLE_ROW_SELECTION);grid_.setSelectionUnit(JSmartGrid.UNIT_ROW); grid_.setSelectionPolicy(JSmartGrid.POLICY_MULTIRANGE);
The data displayed in the former examples had been created to display data stored in memory. Now we'll retrieve data from a database.
The example extends the GenericGridModel 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 method, and is the main part of the setQuery method.
- 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.
- Compile and run DBSnapshotGridDemo. The source files are DBSnapshotGridDemo.java, and DBSnapshotGridModel.java.
- Even though there are no headers yet, the cells' widths were created to be interactively resizable by adding the line:
grid_.setResizable(JSmartGrid.HORIZONTAL,true);- 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 model is then instantiated, and transmitted to the grid: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); }DBSnapshotGridModel model = new DBSnapshotGridModel("jdbc:odbc:base", "sun.jdbc.odbc.JdbcOdbcDriver"); model.setQuery("SELECT * FROM DemoTable"); grid_ = new JSmartGrid(model);
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 and HeaderGridModel.java. The model is given a new instance variable:
and the code in setQuery becomes:private AbstractGridModel columnHeaderModel_;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 which, by default, defines a read-only model.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);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 and SizeGridModel.java, sizes can be set for columns or rows using the preferred, minimal and maximum ruler models.
Now we'll show how to display a TableModel in a JSmartGrid
The source code is in TableToGridDemo.java and TableToGridMapper.java. 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.
- input
- A TableModel
- An optional TableColumnModel
- output
- A GridModel (the instance of the class itself)
- A secondary GridModel for the column headers
The initial connection to the table model is established by the following code:public class TableToGridMapper extends AbstractGridModel implements TableModelListener, TableColumnModelListener { private TableModel source_; private TableColumnModel columns_; private GridModel columnHeaderModel_; ... }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 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(); } }public GridModel getColumnHeaderModel() { return columnHeaderModel_; }Note that the code is mainly delegation. The notification is done by the following method, part of the TableModelListener API: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); }The code in red handles extreme cases that may be generated by TableModels and must not be neglected.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 TableColumnModel, if any, is connected through:
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 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(); } } }All of this is put together in the main code. First, table models are created:public void columnAdded(TableColumnModelEvent e) { fireGridColumnsInserted(e.getToIndex(),1); } public void columnRemoved(TableColumnModelEvent e) { fireGridColumnsDeleted(e.getFromIndex(),1); } public void columnMoved(TableColumnModelEvent e) { fireGridModelChanged(); }Then the grid is built on them: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); }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);
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.
The source code is in StyleGridDemo.java. To do this we don't need to overload the DefaultStyleModel class, we can just modify the renderer attached to numeric values by extending the DefaultStyleModel.NumberRenderer class:
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: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; } } );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);
Now w'll demonstrate how to implement a "choose color" cell.
The source is in WComboGridDemo.java. 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:
Then, we define a renderer class: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; } }Next, we attach it to the DefaultStyleModel thus: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()); } }After filling a combo with the color names, we attach an editor:DefaultStyleModel styleModel = new DefaultStyleModel(); styleModel.setRenderer(NamedColor.class, new NamedColorCellRenderer());Finally, we need to tell the grid to use this modified style:styleModel.setEditor(NamedColor.class, new DefaultGridCellEditor(comboBox));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_.setStyleModel(styleModel);.grid_ = new JSmartGrid(new GenericGridModel(maxRow,10) { public boolean isCellEditable(int row,int column) { return column != 1; } });
Now we are going to explore the spanning possibilities, i.e. the possibility of merging several cells in a rectangular area.
First we show how to span cells to form regular geometrical patterns. The source code is in DirectSpanGridDemo.java. 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; } });
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:
To accomplish this, the grid model of the HeaderGridDemo example will be rewritten. The source code is in QuerySpanGridDemo.java and QuerySpanGridModel.java. First we introduce a new boolean instance variable in order to switch between normal and spanned modes:
Some of the methods of GridModel will be redefined.private boolean spanned_;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 :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); }Moreover, "Group Header" rows must be added or removed as spanning is turned on and off. This can be done as follow: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);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.This SpanModel instance will be made available to the QuerySpanGridDemo.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(); } }The QuerySpanGridDemo will keep a reference to the model as an instance variable:private AbstractSpanModel spanModel_=new SpannedViewSpanModel(); public SpanModel getSpanModel() { return spanModel_; }To create the grid, we use the folowwing:private QuerySpanGridModel model_;The changes, again appearing in bold, are: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);To activate and deactivate the spanning, we need a menu item:
- the query order on the first column.
- the use of a JSmartGrid constructor with two arguments.
and the item then triggers the right method:private JCheckBoxMenuItem spanMenuItem = new JCheckBoxMenuItem("Span");spanMenuItem.addActionListener( new ActionListener () { public void actionPerformed (ActionEvent evt) { model_.setSpanned(spanMenuItem.getState()); } } );
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.
The source code is in CollectionSpanGridDemo.java. 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 object.
Then we add the following utility method to span or unspan the currently selected cells (we assume the selection mode is SINGLE_RECTANGLE_SELECTION):private GenericCollectionSpanModel spanModel_ = new GenericCollectionSpanModel();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: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 grid itself must be initialized in the following way: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 last statement, in blue, is necessary so that the GenericCollectionSpanModel will be notified whenever some structural change occurs in the GridModel.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_);
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.
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 and ColumnDragController.java).
We first define a controller, a class that implements the GridListener interface.
The drag starts when the controller intercepts the mouse pressed event:public class ColumnDragController extends GridAdapter { private int pos_; ... }The mouse dragged event follows: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(); } }And then, the mouse released event: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; } }The key statement is the one in red. This calls to setDraggingDistance which both redisplays the "shifted" cells and fires ruler model events. The GenericGridModel used by default by the JSmartGrid is ready to handle these events, which means that, in this case, it is a RulerModelListener and can handle RULER_ITEMS_MOVED. The following code is added to the ColumnDragGridDemo to connect the grid and the controller:public void gridMouseReleased(GridEvent e) { JSmartGrid grid=(JSmartGrid)e.getSource(); if (grid.isDragging(JSmartGrid.HORIZONTAL)) grid.resetDragging(JSmartGrid.HORIZONTAL); }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.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()); }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 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 .
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 method fires a RulerModelEvent.RULER_ITEMS_MOVED, 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 and DirectColumnDragController.java). The controller class becomes:
When the mouse is pressed, it registers as VetoableChangeListener:public class DirectColumnDragController extends GridAdapter implements VetoableChangeListener { private int pos_; ... }The mouse drag handling method is not modified directly, so we add the following veto: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); } }On the mouse release, the controller must both unregister itself as a VetoableChangeListener and make a last, non-vetoed call to setDraggingDistance.public void vetoableChange(PropertyChangeEvent e) throws PropertyVetoException { if ("columnDragging".equals(e.getPropertyName())) throw new PropertyVetoException("",e); }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); } }
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:
Now we open a spreadsheet application, with an area of nine cells, filled and selected:
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:
The source code is in ClipboardGridDemo.java) but first we will need to use a particular package:
We then define instance variables to identify the area of interest:import java.awt.datatransfer.*;Next, we define a new method:private int lastRow_; private int firstRow_; private int lastColumn_; private int firstColumn_;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: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(); } }The popup itself is connected to the grid and is responsible for storing valid values into the firstRow_... instance variables: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); } } );Transferring data the opposite way is just as easy. We define a new method: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); } } } } );Next, we add a new popup menu item: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); }The grid itself is built exactly like in the free-form spans example.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);
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!