/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ro.nextreports.designer.grid; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.KeyEvent; import java.util.EventObject; import java.util.HashMap; import javax.swing.JComponent; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.Scrollable; import javax.swing.SwingConstants; import javax.swing.UIDefaults; import javax.swing.UIManager; import javax.swing.event.CellEditorListener; import javax.swing.event.ChangeEvent; import ro.nextreports.designer.grid.event.GridModelEvent; import ro.nextreports.designer.grid.event.GridModelListener; import ro.nextreports.designer.grid.plaf.BasicGridUI; import ro.nextreports.designer.grid.plaf.GridUI; /** * The JGrid is used to display and edit regular two-dimensional grid of cells. * Unlike JTable which is fundamentally vertical in that the structure is * determined in the columns, while the rows contain the data. JGrid is * symmetrical with respect to the vertical and the horizontal orientations. * JGrid also allows for cells to be merged into bigger rectangular arrays, * called spans. * * @author Decebal Suiu */ public class JGrid extends JComponent implements Scrollable, CellEditorListener, GridModelListener { /* Default sizes */ public static final int DEFAULT_ROW_HEIGHT = 20; public static final int DEFAULT_COLUMN_WIDTH = 75; protected HeaderModel rowHeaderModel; protected HeaderModel columnHeaderModel; protected GridModel model; protected SelectionModel selectionModel; protected SpanModel spanModel; protected GridRepaintManager repaintManager; private HashMap<Class<Object>, GridCellEditor> editors; private HashMap<Class<Object>, GridCellRenderer> renderers; private Color gridColor; private boolean showGrid = true; /** * @see #getUIClassID * @see #readObject */ private static final String uiClassID = "GridUI"; // Install UI delegate static { UIManager.getDefaults().put(uiClassID, BasicGridUI.class.getName()); } /** * Used by the <code>Scrollable</code> interface to determine the initial * visible area. */ protected Dimension preferredViewportSize; /** * Used to stop recusive call in processKeyBinding. */ private boolean reentrantCall; /** * If editing, the <code>Component</code> that is handling the editing. */ protected Component editorComponent; /** * The object that overwrites the screen real estate occupied by the current * cell and allows the user to change its contents. */ protected GridCellEditor cellEditor; /** * Identifies the column of the cell being edited. */ protected int editingColumn = -1; /** * Identifies the row of the cell being edited. */ protected int editingRow = -1; public JGrid() { this(10, 10); } public JGrid(int rows, int columns) { this(new DefaultGridModel(rows, columns)); } public JGrid(GridModel gridModel) { this(gridModel, new DefaultSpanModel()); } public JGrid(GridModel gridModel, SpanModel spanModel) { this(gridModel, spanModel, new DefaultHeaderModel(gridModel.getRowCount(), DEFAULT_ROW_HEIGHT, SwingConstants.VERTICAL), new DefaultHeaderModel(gridModel.getColumnCount(), DEFAULT_COLUMN_WIDTH, SwingConstants.HORIZONTAL), new DefaultSelectionModel()); } public JGrid(GridModel gridModel, SpanModel spanModel, HeaderModel rowModel, HeaderModel columnModel, SelectionModel selectionModel) { create(gridModel, spanModel, rowModel, columnModel, selectionModel); updateUI(); } protected void create(GridModel model, SpanModel spanModel, HeaderModel rowModel, HeaderModel columnModel, SelectionModel selectionModel) { this.model = model; this.spanModel = spanModel; this.rowHeaderModel = rowModel; this.columnHeaderModel = columnModel; this.selectionModel = selectionModel; this.model.addGridModelListener(this); repaintManager = new GridRepaintManager(this); createDefaults(); updateRepaintManager(); setOpaque(true); } protected void createDefaults() { editors = new HashMap<Class<Object>, GridCellEditor>(); renderers = new HashMap<Class<Object>, GridCellRenderer>(); GridCellRenderer defaultRenderer = new DefaultGridCellRenderer(); renderers.put(Object.class, defaultRenderer); GridCellEditor defaultEditor = new DefaultGridCellEditor(new JTextField()); editors.put(Object.class, defaultEditor); } protected void updateRepaintManager() { rowHeaderModel.addHeaderModelListener(repaintManager); columnHeaderModel.addHeaderModelListener(repaintManager); selectionModel.addSelectionModelListener(repaintManager); spanModel.addSpanModelListener(repaintManager); model.addGridModelListener(repaintManager); } /** * Returns the color used to draw grid lines. * * @return the <code>Color</code> used to draw grid lines */ public Color getGridColor() { return gridColor; } /** * Sets the color used to draw grid lines. * * @param gridColor * the new <code>Color</code> of the grid lines */ public void setGridColor(Color gridColor) { this.gridColor = gridColor; repaint(); } /** * Returns true if the grid draws grid lines. * * @return true if the grid draws grid lines. */ public boolean getShowGrid() { return showGrid; } /** * Sets wether grid lines should be drawn. * * @param show true if grid lines are to be drawn. */ public void setShowGrid(boolean show) { showGrid = show; repaint(); } // end JavaBean properties /** * Returns the number of rows in this grid's model. * * @return the number of rows in this grid's model. */ public int getRowCount() { return model.getRowCount(); } /** * Returns the number of columns in this grid's model. * * @return the number of columns in this grid's model. */ public int getColumnCount() { return model.getColumnCount(); } /** * Returns the height (in pixels) of <code>row</code>. * * @param row * the row whose height is to be returned * * @return the height (in pixels) of <code>row</code> */ public int getRowHeight(int row) { return rowHeaderModel.getSize(row); } /** * Returns the width (in pixels) of <code>column</code>. * * @param column * the column whose width is to be returned * * @return the width (in pixels) of <code>column</code> */ public int getColumnWidth(int column) { return columnHeaderModel.getSize(column); } /** * Sets the height for <code>row</code> to <code>height</code>. * * @param row * the row whose height is to be changed * @param height * new row height (in pixels) */ public void setRowHeight(int row, int height) { rowHeaderModel.setSize(row, height); } /** * Sets the width for <code>column</code> to <code>width</code>. * * @param column * the column whose width is to be changed * @param width * new column width (in pixels) */ public void setColumnWidth(int column, int width) { columnHeaderModel.setSize(column, width); } /** * Return the top vertical coordinate (in pixels) of row. */ public int getRowPosition(int row) { return rowHeaderModel.getPosition(row); } /** * Return the left horizontal coordinate (in pixels) of column. */ public int getColumnPosition(int column) { return columnHeaderModel.getPosition(column); } /** * Get cell bounds of (row, column). */ public Rectangle getCellBounds(int row, int column) { int rowCount = 1; int columnCount = 1; if (isCellSpan(row, column)) { CellSpan span = spanModel.getSpanOver(row, column); row = span.getRow(); column = span.getColumn(); rowCount = span.getRowCount(); columnCount = span.getColumnCount(); } Rectangle cellBounds = new Rectangle(); cellBounds.y = getRowPosition(row); cellBounds.x = getColumnPosition(column); // Height and width include spanned rows and columns for (int i = 0; i < rowCount; i++) { cellBounds.height += getRowHeight(row + i); } for (int j = 0; j < columnCount; j++) { cellBounds.width += getColumnWidth(column + j); } return cellBounds; } /** * Return the row at the specified point. */ public int rowAtPoint(Point point) { return rowHeaderModel.getIndex(point.y); } /** * Return the column at the specified point. */ public int columnAtPoint(Point point) { return columnHeaderModel.getIndex(point.x); } public boolean isCellSpan(int row, int column) { return spanModel.isCellSpan(row, column); } public GridCellRenderer getCellRenderer(int row, int column) { Object value = model.getValueAt(row, column); Class type = Object.class; if (value != null) { type = model.getValueAt(row, column).getClass(); } return getCellRenderer(type, row, column); } public GridCellEditor getCellEditor(int row, int column) { Object value = model.getValueAt(row, column); Class type = Object.class; if (value != null) { type = model.getValueAt(row, column).getClass(); } return getCellEditor(type, row, column); } public GridCellEditor getCellEditor(Class clazz, int row, int column) { GridCellEditor editor = editors.get(clazz); if (editor != null) { return editor; } else { return getCellEditor(clazz.getSuperclass(), row, column); } } @SuppressWarnings("unchecked") public void setCellEditor(Class clazz, GridCellEditor editor) { editors.put(clazz, editor); } public GridCellRenderer getCellRenderer(Class clazz, int row, int column) { GridCellRenderer renderer = renderers.get(clazz); if (renderer != null) { return renderer; } else { return getCellRenderer(clazz.getSuperclass(), row, column); } } @SuppressWarnings("unchecked") public void setCellRenderer(Class clazz, GridCellRenderer renderer) { renderers.put(clazz, renderer); } /** * Prepares the renderer for painting cell(row,column) */ public Component prepareRenderer(GridCellRenderer renderer, int row, int column) { Object value = model.getValueAt(row, column); boolean isSelected = isSelected(row, column); Cell selectedCell = selectionModel.getSelectedCell(); boolean hasFocus = (selectedCell != null) && (selectedCell.getRow() == row) && (selectedCell.getColumn() == column) && isFocusOwner(); return renderer.getRendererComponent(row, column, value, isSelected, hasFocus, this); } /** * Prepares the editor for cell(row, column). */ public Component prepareEditor(GridCellEditor editor, int row, int column) { Object value = model.getValueAt(row, column); boolean isSelected = isSelected(row, column); return editor.getEditorComponent(row, column, value, isSelected, this); } public boolean isSelected(int row, int column) { return selectionModel.isSelected(row, column); } public void ensureCellInVisibleRect(int row, int column) { Rectangle cellRect = getCellBounds(row, column); scrollRectToVisible(cellRect); } public GridModel getModel() { return model; } public void setModel(GridModel model) { this.model.removeGridModelListener(this); this.model.removeGridModelListener(repaintManager); this.model = model; this.model.addGridModelListener(this); this.model.addGridModelListener(repaintManager); repaintManager.resizeAndRepaint(); } public SelectionModel getSelectionModel() { return selectionModel; } public void setSelectionModel(SelectionModel model) { selectionModel.removeSelectionModelListener(repaintManager); selectionModel = model; selectionModel.addSelectionModelListener(repaintManager); repaintManager.repaint(); } public SpanModel getSpanModel() { return spanModel; } public void setSpanModel(SpanModel model) { spanModel.removeSpanModelListener(repaintManager); spanModel = model; spanModel.addSpanModelListener(repaintManager); repaintManager.repaint(); } public HeaderModel getRowHeaderModel() { return rowHeaderModel; } public void setRowHeaderModel(HeaderModel model) { rowHeaderModel.removeHeaderModelListener(repaintManager); rowHeaderModel = model; rowHeaderModel.addHeaderModelListener(repaintManager); repaintManager.resizeAndRepaint(); } public HeaderModel getColumnHeaderModel() { return columnHeaderModel; } public void setColumnHeaderModel(HeaderModel model) { columnHeaderModel.removeHeaderModelListener(repaintManager); columnHeaderModel = model; columnHeaderModel.addHeaderModelListener(repaintManager); repaintManager.resizeAndRepaint(); } /** * Sync row and column sizes between models. */ public void gridChanged(GridModelEvent event) { int eventType = event.getType(); if (eventType == GridModelEvent.ROWS_INSERTED) { if (rowHeaderModel instanceof ResizableGrid) { ((ResizableGrid) rowHeaderModel).insertRows(event.getFirstRow(), event.getRowCount()); } if (columnHeaderModel instanceof ResizableGrid) { ((ResizableGrid) columnHeaderModel).insertRows(event.getFirstRow(), event.getRowCount()); } if (spanModel instanceof ResizableGrid) { ((ResizableGrid) spanModel).insertRows(event.getFirstRow(), event.getRowCount()); } repaintManager.resizeAndRepaint(); } else if (eventType == GridModelEvent.ROWS_DELETED) { if (rowHeaderModel instanceof ResizableGrid) { ((ResizableGrid) rowHeaderModel).removeRows(event.getFirstRow(), event.getRowCount()); } if (columnHeaderModel instanceof ResizableGrid) { ((ResizableGrid) columnHeaderModel).removeRows(event.getFirstRow(), event.getRowCount()); } if (spanModel instanceof ResizableGrid) { ((ResizableGrid) spanModel).removeRows(event.getFirstRow(), event.getRowCount()); } repaintManager.resizeAndRepaint(); } else if (eventType == GridModelEvent.COLUMNS_INSERTED) { if (rowHeaderModel instanceof ResizableGrid) { ((ResizableGrid) rowHeaderModel).insertColumns( event.getFirstColumn(), event.getColumnCount()); } if (columnHeaderModel instanceof ResizableGrid) { ((ResizableGrid) columnHeaderModel).insertColumns(event .getFirstColumn(), event.getColumnCount()); } if (spanModel instanceof ResizableGrid) { ((ResizableGrid) spanModel).insertColumns(event .getFirstColumn(), event.getColumnCount()); } repaintManager.resizeAndRepaint(); } else if (eventType == GridModelEvent.COLUMNS_DELETED) { if (rowHeaderModel instanceof ResizableGrid) { ((ResizableGrid) rowHeaderModel).removeColumns( event.getFirstColumn(), event.getColumnCount()); } if (columnHeaderModel instanceof ResizableGrid) { ((ResizableGrid) columnHeaderModel).removeColumns(event .getFirstColumn(), event.getColumnCount()); } if (spanModel instanceof ResizableGrid) { ((ResizableGrid) spanModel).removeColumns(event .getFirstColumn(), event.getColumnCount()); } repaintManager.resizeAndRepaint(); } } public boolean editCellAt(int row, int column) { return editCellAt(row, column, null); } /** * Programmatically starts editing the cell at <code>row</code> and * <code>column</code>, if the cell is editable. * * @param row * the row to be edited * @param column * the column to be edited * @param event * event to pass into shouldSelectCell * @exception IllegalArgumentException * If <code>row</code> or <code>column</code> is not in the * valid range * @return false if for any reason the cell cannot be edited */ public boolean editCellAt(int row, int column, EventObject event) { if ((cellEditor != null) && !cellEditor.stopCellEditing()) { return false; } // Check out of bounds if (row < 0 || row >= getRowCount() || column < 0 || column >= getColumnCount()) { return false; } if (isCellSpan(row, column)) { // Translate cell coords to anchor of span CellSpan span = spanModel.getSpanOver(row, column); row = span.getRow(); column = span.getColumn(); } if (!model.isCellEditable(row, column)) { return false; } GridCellEditor editor = getCellEditor(row, column); if (editor != null && editor.isCellEditable(event)) { editorComponent = prepareEditor(editor, row, column); if (editorComponent == null) { removeEditor(); return false; } editorComponent.setBounds(getCellBounds(row, column)); add(editorComponent); editorComponent.validate(); editorComponent.requestFocus(); cellEditor = editor; setEditingRow(row); setEditingColumn(column); editor.addCellEditorListener(this); return true; } return false; } /** * Discards the editor object and frees the real estate it used for cell * rendering. */ public void removeEditor() { if (cellEditor != null) { cellEditor.removeCellEditorListener(this); requestFocus(); if (editorComponent != null) { remove(editorComponent); } Rectangle cellRectangle = getCellBounds(editingRow, editingColumn); cellEditor = null; setEditingColumn(-1); setEditingRow(-1); editorComponent = null; repaint(cellRectangle); } } /** * Sets the <code>editingColumn</code> variable. * * @param column * the column of the cell to be edited * * @see #editingColumn */ public void setEditingColumn(int column) { editingColumn = column; } /** * Sets the <code>editingRow</code> variable. * * @param row * the row of the cell to be edited * * @see #editingRow */ public void setEditingRow(int row) { editingRow = row; } /** * Returns true if a cell is being edited. * * @return true if the table is editing a cell * @see #editingColumn * @see #editingRow */ public boolean isEditing() { return (cellEditor == null) ? false : true; } /** * Returns the component that is handling the editing session. If nothing is * being edited, returns null. * * @return Component handling editing session */ public Component getEditorComponent() { return editorComponent; } /** * Returns the index of the column that contains the cell currently being * edited. If nothing is being edited, returns -1. * * @return the index of the column that contains the cell currently being * edited; returns -1 if nothing being edited * @see #editingRow */ public int getEditingColumn() { return editingColumn; } /** * Returns the index of the row that contains the cell currently being * edited. If nothing is being edited, returns -1. * * @return the index of the row that contains the cell currently being * edited; returns -1 if nothing being edited * @see #editingColumn */ public int getEditingRow() { return editingRow; } /** * Return the current cell editor */ public GridCellEditor getCurrentCellEditor() { return cellEditor; } /** * Invoked when editing is finished. The changes are saved and the editor is * discarded. * <p> * Application code will not use these methods explicitly, they are used * internally by JGrid. * * @param event * the event received * @see CellEditorListener */ public void editingStopped(ChangeEvent event) { // take in the new value if (cellEditor != null) { Object value = cellEditor.getCellEditorValue(); model.setValueAt(value, editingRow, editingColumn); removeEditor(); requestFocus(); } } /** * Invoked when editing is canceled. The editor object is discarded and the * cell is rendered once again. * * Application code will not use these methods explicitly, they are used * internally by JSpread. * * @param event the event received * @see CellEditorListener */ public void editingCanceled(ChangeEvent event) { removeEditor(); requestFocus(); } @Override protected boolean processKeyBinding(KeyStroke keyStroke, KeyEvent keyEvent, int condition, boolean pressed) { if (reentrantCall) { return false; } reentrantCall = true; boolean retValue = super.processKeyBinding(keyStroke, keyEvent, condition, pressed); // start editing when the ENTER key is typed if (!retValue && (condition == WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) && hasFocus()) { // we do not have a binding for the event. Component component = getEditorComponent(); if (component == null) { // only attempt to install the editor on a KEY_PRESSED if ((keyEvent == null) || (keyEvent.getID() != KeyEvent.KEY_PRESSED)) { reentrantCall = false; return false; } // start when an enter is pressed int code = keyEvent.getKeyCode(); if (code != KeyEvent.VK_ENTER) { reentrantCall = false; return false; } // try to install the editor Cell selectedCell = selectionModel.getSelectedCell(); int row = selectedCell.getRow(); int column = selectedCell.getColumn(); if ((row != -1) && (column != -1) && !isEditing()) { if (!editCellAt(row, column)) { reentrantCall = false; return false; } } component = getEditorComponent(); if (component == null) { reentrantCall = false; return false; } } } reentrantCall = false; return retValue; } /** * Sets the preferred size of the viewport for this table. * * @param size * a <code>Dimension</code> object specifying the * <code>preferredSize</code> of a <code>JViewport</code> whose * view is this spreadsheet * @see Scrollable#getPreferredScrollableViewportSize */ public void setPreferredScrollableViewportSize(Dimension size) { preferredViewportSize = size; } /** * Returns the preferred size of the viewport for this table. * * @return a <code>Dimension</code> object containing the * <code>preferredSize</code> of the <code>JViewport</code> which * displays this table * @see Scrollable#getPreferredScrollableViewportSize */ public Dimension getPreferredScrollableViewportSize() { return preferredViewportSize; } /** * Returns the scroll increment (in pixels) that completely exposes one new * row or column (depending on the orientation). * * This method is called each time the user requests a unit scroll. * * @param visibleRect * the view area visible within the viewport * @param orientation * either <code>SwingConstants.VERTICAL</code> or * <code>SwingConstants.HORIZONTAL</code> * @param direction * less than zero to scroll up/left, greater than zero for * down/right * @return the "unit" increment for scrolling in the specified direction * @see Scrollable#getScrollableUnitIncrement */ public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { if (orientation == SwingConstants.HORIZONTAL) { return DEFAULT_COLUMN_WIDTH; } else { return DEFAULT_ROW_HEIGHT; } } /** * Returns <code>visibleRect.height</code> or <code>visibleRect.width</code> * , depending on this spreadsheet's orientation. * * @return <code>visibleRect.height</code> or <code>visibleRect.width</code> * per the orientation * @see Scrollable#getScrollableBlockIncrement */ public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { if (orientation == SwingConstants.VERTICAL) { return visibleRect.height; } else { return visibleRect.width; } } /** * Returns false to indicate that the width of the viewport does not * determine the width of the spreadsheet. * * @return false * @see Scrollable#getScrollableTracksViewportWidth */ public boolean getScrollableTracksViewportWidth() { return false; } /** * Returns false to indicate that the height of the viewport does not * determine the height of the spreadsheet. * * @return false * @see Scrollable#getScrollableTracksViewportHeight */ public boolean getScrollableTracksViewportHeight() { return false; } /** * Returns the L&F object that renders this component. * * @return GridUI object */ public GridUI getUI() { return (GridUI) ui; } /** * Sets the L&F object that renders this component. * * @param ui * the GridUI L&F object * @see UIDefaults#getUI */ public void setUI(GridUI ui) { if (this.ui != ui) { super.setUI(ui); repaint(); } } /** * Notification from the UIFactory that the L&F has changed. * * @see JComponent#updateUI */ @Override public void updateUI() { setUI((GridUI) UIManager.getUI(this)); resizeAndRepaint(); } /** * Returns a string that specifies the name of the l&f class that renders * this component. * * @return String "GridUI" * * @see JComponent#getUIClassID * @see UIDefaults#getUI */ @Override public String getUIClassID() { return uiClassID; } public void resizeAndRepaint() { revalidate(); repaint(); } public Object getValueAt(int row, int column) { return model.getValueAt(row, column); } public void setValueAt(Object value, int row, int column) { model.setValueAt(value, row, column); } }