0001: // ============================================================================
0002: // $Id: Spreadsheet.java,v 1.16 2005/12/17 04:46:57 davidahall Exp $
0003: // Copyright (c) 2004-2005 David A. Hall
0004: // ============================================================================
0005: // The contents of this file are subject to the Common Development and
0006: // Distribution License (CDDL), Version 1.0 (the License); you may not use this
0007: // file except in compliance with the License. You should have received a copy
0008: // of the the License along with this file: if not, a copy of the License is
0009: // available from Sun Microsystems, Inc.
0010: //
0011: // http://www.sun.com/cddl/cddl.html
0012: //
0013: // From time to time, the license steward (initially Sun Microsystems, Inc.) may
0014: // publish revised and/or new versions of the License. You may not use,
0015: // distribute, or otherwise make this file available under subsequent versions
0016: // of the License.
0017: //
0018: // Alternatively, the contents of this file may be used under the terms of the
0019: // GNU Lesser General Public License Version 2.1 or later (the "LGPL"), in which
0020: // case the provisions of the LGPL are applicable instead of those above. If you
0021: // wish to allow use of your version of this file only under the terms of the
0022: // LGPL, and not to allow others to use your version of this file under the
0023: // terms of the CDDL, indicate your decision by deleting the provisions above
0024: // and replace them with the notice and other provisions required by the LGPL.
0025: // If you do not delete the provisions above, a recipient may use your version
0026: // of this file under the terms of either the CDDL or the LGPL.
0027: //
0028: // This library is distributed in the hope that it will be useful,
0029: // but WITHOUT ANY WARRANTY; without even the implied warranty of
0030: // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
0031: // ============================================================================
0032:
0033: package net.sf.jga.swing.spreadsheet;
0034:
0035: import java.awt.Component;
0036: import java.awt.Container;
0037: import java.awt.Dimension;
0038: import java.awt.Font;
0039: import java.awt.FontMetrics;
0040: import java.awt.Graphics;
0041: import java.awt.Insets;
0042: import java.awt.Point;
0043: import java.awt.Rectangle;
0044: import java.io.IOException;
0045: import java.io.InputStream;
0046: import java.io.OutputStream;
0047: import java.text.MessageFormat;
0048: import java.util.HashMap;
0049: import java.util.HashSet;
0050: import java.util.Iterator;
0051: import java.util.Map;
0052: import java.util.Observable;
0053: import java.util.Observer;
0054: import java.util.Set;
0055: import javax.swing.CellRendererPane;
0056: import javax.swing.JComponent;
0057: import javax.swing.JScrollPane;
0058: import javax.swing.JTable;
0059: import javax.swing.JViewport;
0060: import javax.swing.UIManager;
0061: import javax.swing.border.Border;
0062: import javax.swing.event.ListSelectionEvent;
0063: import javax.swing.table.AbstractTableModel;
0064: import javax.swing.table.JTableHeader;
0065: import javax.swing.table.TableCellEditor;
0066: import javax.swing.table.TableCellRenderer;
0067: import javax.swing.table.TableColumn;
0068: import javax.swing.table.TableColumnModel;
0069: import javax.swing.table.TableModel;
0070: import javax.xml.parsers.ParserConfigurationException;
0071: import javax.xml.parsers.SAXParser;
0072: import javax.xml.parsers.SAXParserFactory;
0073: import javax.xml.transform.OutputKeys;
0074: import javax.xml.transform.Transformer;
0075: import javax.xml.transform.TransformerConfigurationException;
0076: import javax.xml.transform.sax.SAXTransformerFactory;
0077: import javax.xml.transform.sax.TransformerHandler;
0078: import javax.xml.transform.stream.StreamResult;
0079: import net.sf.jga.fn.BinaryFunctor;
0080: import net.sf.jga.fn.EvaluationException;
0081: import net.sf.jga.fn.Generator;
0082: import net.sf.jga.fn.UnaryFunctor;
0083: import net.sf.jga.fn.adaptor.Constant;
0084: import net.sf.jga.fn.adaptor.Identity;
0085: import net.sf.jga.parser.IParser;
0086: import net.sf.jga.parser.JFXGParser;
0087: import net.sf.jga.parser.FunctorRef;
0088: import net.sf.jga.parser.GeneratorRef;
0089: import net.sf.jga.parser.ParseException;
0090: import org.xml.sax.Attributes;
0091: import org.xml.sax.InputSource;
0092: import org.xml.sax.SAXException;
0093: import org.xml.sax.XMLReader;
0094: import org.xml.sax.helpers.AttributesImpl;
0095: import org.xml.sax.helpers.DefaultHandler;
0096:
0097: /**
0098: * Table that contains a sparse-matrix of functors (Generators, specifically).
0099: * Each cell in the table can contain a formula, and the table can generate
0100: * references to cells to be used in formulas in other cells. Cells in the
0101: * matrix can contain constant values and arbitrary Generators, but to support
0102: * editing, the calling program must pass an expression that can be parsed
0103: * via a JFXGParser.
0104: * @see net.sf.jga.parser.JFXGParser
0105: * <p>
0106: * Copyright © 2004-2005 David A. Hall
0107: * @author <a href="mailto:davidahall@users.sf.net">David A. Hall</a>
0108: */
0109:
0110: // TODO: implement cell ranges as a collection
0111: // TODO: implement drag/drop of values
0112: // TODO: make the cells editable by storing their formula's in string form
0113: // as well as in parsed form (if possible??)
0114: // TODO: get error reporting working (renderer should know about cells with
0115: // errors)
0116: // TODO: add a 'formula' mode, where the renderer shows the formula of the
0117: // cell rather than its value
0118: // TODO: allow cells to be 'manually' registered as dependencies -- when a cell's value is set
0119: // or manipulated as the result of the formula in some other cell, the first cell does not know
0120: // that it has been updated, so it does not notify the table or any of the cells that depend on
0121: // it. We can register on all method invocations that start with a 'set' prefix, but (for
0122: // example) if a cell contains a point, and another cell calls the point's "move(x,y)" method,
0123: // the point won't know that it was updated.
0124: // TODO: think about assignment statements (include operation/assignment (+=, ...)) where a
0125: // cell reference or member reference is allowed on the lhs.
0126: // TODO: remove the dependency on GenericCellRenderer: cells should 'pre-render' the value
0127: // to String (or whatever comes out of the Format functor) when either the value or format is
0128: // changed, and serve the cached value to the table model (for performance reasons).
0129: // Alternatively, the model may cache such values and clear the cache on value/format changes.
0130: public class Spreadsheet extends JTable {
0131:
0132: static final long serialVersionUID = -4784933072621672138L;
0133:
0134: // Parser used to handle formulas. This version adds the 'cell' functor, to provide for cell
0135: // references, and the 'row' and 'col' keywords to provide for relative cell addressing
0136: transient Parser _parser = new Parser();
0137:
0138: // a pre-cast reference to the model
0139: private SpreadsheetTableModel _model;
0140:
0141: // Component used as a row header for the table
0142: private RowHeader _rowHeader;
0143:
0144: // Functor used to display status information
0145: private UnaryFunctor<String, ?> _statusFn = new Identity<String>();
0146:
0147: // An unfortunate bootstrap hack: we need to allow for arbitrary models
0148: // during construction so that the SpreadsheetModel can be non-static and
0149: // thus have access to the enclosing Spreadsheet object.
0150: private boolean _initialized;
0151:
0152: public Spreadsheet(int rows, int cols) {
0153: super ();
0154: _model = new SpreadsheetTableModel(rows, cols);
0155: super .setModel(_model);
0156:
0157: setAutoCreateColumnsFromModel(false);
0158: setAutoResizeMode(AUTO_RESIZE_OFF);
0159: setCellSelectionEnabled(true);
0160: setCellEditor(new Cell.Editor());
0161:
0162: TableColumnModel columns = getColumnModel();
0163: for (int i = 0; i < cols; ++i) {
0164: columns.getColumn(i).setHeaderValue(String.valueOf(i));
0165: }
0166:
0167: getTableHeader().setReorderingAllowed(false);
0168:
0169: _rowHeader = new RowHeader(this );
0170:
0171: _parser.bindThis(this );
0172:
0173: _initialized = true;
0174: }
0175:
0176: // - - - - - - - - - -
0177: // Spreadsheet Config
0178: // - - - - - - - - - -
0179:
0180: public void setColumnCount(int width) {
0181: int oldWidth = getColumnCount();
0182: doSetColumnCount(oldWidth, width);
0183: if (width > 0 && width != oldWidth) {
0184: _model.setColumnCount(width);
0185: }
0186: }
0187:
0188: private void doSetColumnCount(int oldWidth, int width) {
0189: if (width > 0 && width != oldWidth) {
0190: TableColumnModel columns = getColumnModel();
0191: if (oldWidth < width) {
0192: for (int i = oldWidth; i < width; ++i) {
0193: TableColumn column = new TableColumn(i);
0194: column.setHeaderValue(String.valueOf(i));
0195: addColumn(column);
0196: }
0197: } else {
0198: for (int i = oldWidth - 1; i >= width; --i) {
0199: removeColumn(columns.getColumn(i));
0200: }
0201: }
0202: }
0203: }
0204:
0205: public void setRowCount(int height) {
0206: if (height > 0 && height != getRowCount()) {
0207: _model.setRowCount(height);
0208: _rowHeader.setRowCount(height);
0209: }
0210: }
0211:
0212: /**
0213: * Returns the parser used by the spreadsheet.
0214: */
0215: public IParser getParser() {
0216: return _parser;
0217: }
0218:
0219: // The type of uninitialized cells
0220: private Class _defaultType = Integer.class;
0221:
0222: /**
0223: * Returns the type of cells that have not be initialized.
0224: */
0225: public Class getDefaultCellType() {
0226: return _defaultType;
0227: }
0228:
0229: /**
0230: * Sets the type returned by cells that have not been initialized. By default,
0231: * this value is java.lang.Integer. If the default value is non-null and is not
0232: * an instance of the given type, then the default value will be set to null as
0233: * a side-effect of setting the type.
0234: */
0235: public void setDefaultCellType(Class type) {
0236: _defaultType = type;
0237: if (_defaultValue != null && !type.isInstance(_defaultValue))
0238: _defaultValue = null;
0239: }
0240:
0241: // The value of uninitialized cells
0242: private Object _defaultValue = new Integer(0);
0243:
0244: /**
0245: * Returns the default value of cells that have not been initialized.
0246: */
0247: public Object getDefaultCellValue() {
0248: return _defaultValue;
0249: }
0250:
0251: /**
0252: * Sets the value returned by cells that have not been initialized. By default,
0253: * this value is a java.lang.Integer.ZERO. When called, if the new value is not
0254: * an instance of the existing default type, then the default type will be changed
0255: * to the class of the new value as a side-effect.
0256: */
0257: public void setDefaultCellValue(Object value) {
0258: _defaultValue = value;
0259: if (!_defaultType.isInstance(value))
0260: _defaultType = value.getClass();
0261: }
0262:
0263: /**
0264: * Sets the option that determines if empty cells and newly created cells are
0265: * editable.
0266: */
0267: public void setEditableByDefault(boolean b) {
0268: _model.setEditableByDefault(b);
0269: }
0270:
0271: /**
0272: * Returns true if empty and newly created cells are editable.
0273: */
0274: public boolean isEditableByDefault() {
0275: return _model.isEditableByDefault();
0276: }
0277:
0278: // Flag that controls whether cells are strongly or weakly typed.
0279: private boolean _strictType;
0280:
0281: /**
0282: * Returns true if cells strictly enforce types once a type has been set, or if
0283: * they allow their types to be changed once set.
0284: */
0285:
0286: public boolean isStrictlyTyped() {
0287: return _strictType;
0288: }
0289:
0290: /**
0291: * Sets the option that controls whether cells will be strongly or weakly typed.
0292: * Strongly typed cells will not change their types after being set: a new formula
0293: * must return the same type as the type that the cell has been assigned. Weakly
0294: * typed cells can have their types changed at any time. By default, cells are
0295: * weakly typed.
0296: */
0297:
0298: public void setStrictTyping(boolean b) {
0299: _strictType = b;
0300: }
0301:
0302: // - - - - - - - - - -
0303: // I/O methods
0304: // - - - - - - - - - -
0305:
0306: /**
0307: */
0308: public void readSpreadsheet(InputStream is) throws IOException {
0309: new Reader().readSpreadsheet(is);
0310: }
0311:
0312: /**
0313: */
0314: public void writeSpreadsheet(OutputStream os) throws IOException {
0315: new Writer().writeSpreadsheet(os);
0316: }
0317:
0318: // - - - - - - - - - -
0319: // GUI methods
0320: // - - - - - - - - - -
0321:
0322: /**
0323: * Returns the component used as a row header
0324: */
0325:
0326: public JComponent getRowHeader() {
0327: return _rowHeader;
0328: }
0329:
0330: /**
0331: * Sets the functor used by the spreadsheet to display status information. This
0332: * allows the spreadsheet's container to (for example) route status information
0333: * from the spreadsheet to a log file, or to a status bar.
0334: */
0335:
0336: public void setStatusHandler(UnaryFunctor<String, ?> fn) {
0337: _statusFn = fn;
0338: }
0339:
0340: /**
0341: * Updates the spreadsheet's status message.
0342: */
0343: public void setStatus(String status) {
0344: _statusFn.fn(status);
0345: }
0346:
0347: // -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
0348: // This is temporary, as it really won't support undo/redo -- need to identify
0349: // the cell involved and create an UndoableEdit. I think its going to end up
0350: // that the Cell class creates the UndoableEdits and sends them to the sheet,
0351: // which passes them along to any listeners that may register.
0352: // -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
0353:
0354: private Generator<?> _updateFn;
0355:
0356: public void setUpdateHandler(Generator<?> fn) {
0357: _updateFn = fn;
0358: }
0359:
0360: private void fireSpreadsheetUpdated() {
0361: if (_updateFn != null)
0362: _updateFn.gen();
0363: }
0364:
0365: // - - - - - - - - - -
0366: // Cell access methods
0367: // - - - - - - - - - -
0368:
0369: /**
0370: * Discards all information about the given cell.
0371: */
0372:
0373: public void clearCellAt(int row, int col) {
0374: Cell cell = getCellIfPresent(row, col);
0375: if (cell != null) {
0376: cell.clear();
0377: }
0378: }
0379:
0380: /**
0381: * Returns the cell at the given address, creating one if one does not already
0382: * exist.
0383: * @throws IndexOutOfBoundsException if the row or column is out of bounds
0384: */
0385: public Cell getCellAt(int row, int col) {
0386: Cell cell = getCellIfPresent(row, col);
0387: if (cell == null) {
0388: cell = _model.setCell(new Cell(this , row, col));
0389: }
0390: return cell;
0391: }
0392:
0393: /**
0394: * Returns the cell at the given address if one exists, or null if it does not
0395: * @throws IndexOutOfBoundsException if the row or column is out of bounds
0396: */
0397: Cell getCellIfPresent(int row, int col) {
0398: return _model.getCellAt(row, col);
0399: }
0400:
0401: // There's no universal way to convert a constant value of some arbitrary
0402: // class into a Generator if the cell is later edited. That is, suppose
0403: // we allow the cell to be edited: there's no universal string that we
0404: // could put into the editor that we'd be able to parse back out. What
0405: // we'd need is the expression that the program uses to generate the value,
0406: // which we can then parse into the correct Generator.
0407: // For example, suppose a Cell is intended to hold a Color: this method
0408: // could be called to set the value to Color.RED, but we can't know how to
0409: // generate the expression "Color.RED" in order for it to be edited.
0410:
0411: /**
0412: * Builds a read-only cell to hold the given value. If you need the cell
0413: * to be editable
0414: * @throws IndexOutOfBoundsException if the row or column is out of bounds
0415: */
0416: public <T> Cell setCellAt(Class<T> type, T value, int row, int col) {
0417: return _model.setCellAt(type, new Constant<T>(value), row, col);
0418: }
0419:
0420: // TODO: Write a visitor for the generator to create the correct formula (if possible).
0421: // In the face of user-supplied functors, that may not work (it may require a number
0422: // of registrations for the user-supplied functor).
0423:
0424: /**
0425: * Builds a cell to hold the given value
0426: * @throws IndexOutOfBoundsException if the row or column is out of bounds
0427: */
0428: public <T> Cell setCellAt(Class<T> type, Generator<T> gen, int row,
0429: int col) {
0430: return _model.setCellAt(type, gen, row, col);
0431: }
0432:
0433: // /**
0434: // * Builds a read-only cell to hold the given value
0435: // * @throws IndexOutOfBoundsException if the row or column is out of bounds
0436: // * @throws IllegalArgumentException if the cell has already been set
0437: // */
0438: // private <T> Cell setCellAt(Class<T> type, Generator<T> gen, int row, int col) {
0439: // // TODO: edit existing cells, changing their types as necessary
0440:
0441: // // @SuppressWarnings
0442: // // the Cell returned by getCellAt is non-generic.
0443: // Cell cell = getCellAt(row,col);
0444: // if (cell != null) {
0445: // throw new IllegalArgumentException(cell+" has already been set");
0446: // }
0447:
0448: // Point p = new Point(row, col);
0449: // cell = new Cell(Spreadsheet.this, type, p, gen);
0450: // _model.setCellAt(p, cell);
0451: // return cell;
0452: // }
0453:
0454: /**
0455: * Builds a possibly editable cell to hold the given formula.
0456: * @throws IndexOutOfBoundsException if the row or column is out of bounds
0457: */
0458: public Cell setCellAt(String formula, int row, int col) {
0459: return _model.setCellAt(formula, row, col, _model
0460: .isEditableByDefault());
0461: }
0462:
0463: /**
0464: * Builds a possibly editable cell to hold the given formula.
0465: * @throws IndexOutOfBoundsException if the row or column is out of bounds
0466: */
0467: public Cell setCellAt(String formula, int row, int col,
0468: boolean editable) {
0469: return _model.setCellAt(formula, row, col, editable);
0470: }
0471:
0472: // /**
0473: // * Builds a possibly editable cell to hold the given formula
0474: // * @throws IndexOutOfBoundsException if the row or column is out of
0475: // * bounds
0476: // * @throws IllegalArgumentException if the cell has already been set
0477: // */
0478: // private Cell setCellAt(String formula, int row,int col, boolean editable){
0479: // // TODO: edit existing cells, changing their types as necessary
0480:
0481: // // @SuppressWarnings
0482: // // the Cell returned by getCellAt is non-generic.
0483: // Cell cell = getCellAt(row,col);
0484: // if (cell != null) {
0485: // throw new IllegalArgumentException(cell+" has already been set");
0486: // }
0487:
0488: // Point p = new Point(row, col);
0489: // cell = new Cell(this, p, formula, editable);
0490: // _model.setCellAt(p, cell);
0491: // return cell;
0492: // }
0493:
0494: /**
0495: * Replaces the CellRenderer for the given cell such that the given
0496: * formatting functor is used to present the contents of the cell.
0497: * NOTE: Use this method cautiously, as passing a formatter of an
0498: * inappropriate type can lead to a class cast exception or to the
0499: * Cell reporting '### CLASS ###' condition.
0500: */
0501: public <T> void setFormatAt(UnaryFunctor<T, String> formatter,
0502: int row, int col) {
0503: // @SuppressWarnings
0504: // the Cell returned by getCellAt is non-generic, so there's no check here
0505: // that the cell is of the proper type T. The preferred mechanism is to
0506: // set the format of the cell given a Generic reference. The Cell class
0507: // catches ClassCastException when it is evaluated, so while this isn't
0508: // typesafe, the effects are trapped
0509: getCellAt(row, col).setFormat(formatter);
0510: }
0511:
0512: /**
0513: * Returns the cell with the given name, or null if no cell has the given name.
0514: */
0515: public Cell getCellByName(String name) {
0516: return _model.getCellByName(name);
0517: }
0518:
0519: /**
0520: * Sets the name of the given cell.
0521: * @throws IllegalArgumentException if the name is already in use.
0522: */
0523: public Cell setCellName(String name, int row, int col) {
0524: Cell cell = _model.setCellName(name, row, col);
0525: setStatus(cell.toString());
0526: return cell;
0527: }
0528:
0529: /**
0530: * Returns a reference to the contents of a given cell
0531: */
0532: public <T> Generator<T> getReference(Class<T> type, int row, int col) {
0533: return _model.getReference(type, row, col);
0534: }
0535:
0536: // - - - - - - - - - - - - - -
0537: // other model wrapper methods
0538: // - - - - - - - - - - - - - -
0539:
0540: public void clear() {
0541: _model.clear();
0542: setRowSelectionInterval(0, 0);
0543: setColumnSelectionInterval(0, 0);
0544: }
0545:
0546: // - - - - - - - - - - - - - -
0547: // JTable interface
0548: // - - - - - - - - - - - - - -
0549:
0550: public TableCellEditor getCellEditor(int row, int col) {
0551: Cell cell = getCellIfPresent(row, col);
0552: if (cell == null) {
0553: return super .getCellEditor(row, col);
0554: }
0555:
0556: TableCellEditor editor = cell.getEditor();
0557: return (editor != null) ? editor : super
0558: .getCellEditor(row, col);
0559: }
0560:
0561: private TableCellRenderer _defaultRenderer = new Cell.Renderer();
0562:
0563: public TableCellRenderer getCellRenderer(int row, int col) {
0564: Cell cell = getCellIfPresent(row, col);
0565: if (cell == null)
0566: return _defaultRenderer;
0567: // return super.getCellRenderer(row,col);
0568:
0569: TableCellRenderer renderer = cell.getRenderer();
0570: return (renderer != null) ? renderer : _defaultRenderer; //super.getCellRenderer(row,col);
0571: }
0572:
0573: public void setModel(TableModel model) {
0574: // Only true during the constructor: _initialized will be true after the
0575: // the constructor is finished. Otherwise, the model must be a
0576: // SpreadsheetTableModel
0577: if (!_initialized)
0578: super .setModel(model);
0579: else if (model instanceof SpreadsheetTableModel) {
0580: super .setModel(model);
0581: SpreadsheetTableModel oldModel = _model;
0582: _model = (SpreadsheetTableModel) model;
0583: _rowHeader.setRowCount(_model.getRowCount());
0584: doSetColumnCount(oldModel.getColumnCount(), _model
0585: .getColumnCount());
0586: } else {
0587: // we _could_ write an adaptor that bridges an arbitrary table model
0588: // to a spreadsheet table model.
0589: String msg = "Spreadsheet requires SpreadsheetTableModel";
0590: throw new IllegalArgumentException(msg);
0591: }
0592: }
0593:
0594: /**
0595: * In addition to the JTable behaviour of this method (which takes care to add the
0596: * table's columnheader to an enclosing scrollpane's viewport), add a rowheader to
0597: * the enclosing scrollpane's viewport as well.
0598: */
0599: protected void configureEnclosingScrollPane() {
0600: super .configureEnclosingScrollPane();
0601: // The base class logic is reproduced: we only want to set the row header under
0602: // the same conditions as those in which the column header will be set
0603: Container p = getParent();
0604: if (p instanceof JViewport) {
0605: Container gp = p.getParent();
0606: if (gp instanceof JScrollPane) {
0607: JScrollPane scrollPane = (JScrollPane) gp;
0608: JViewport viewport = scrollPane.getViewport();
0609: if (viewport == null || viewport.getView() != this ) {
0610: return;
0611: }
0612:
0613: scrollPane.setRowHeaderView(getRowHeader());
0614: }
0615: }
0616: }
0617:
0618: // Overrides the base class to set the status information on selection changes
0619: public void valueChanged(ListSelectionEvent e) {
0620: if (!e.getValueIsAdjusting())
0621: showSelectionStatus();
0622:
0623: super .valueChanged(e);
0624: }
0625:
0626: // Overrides the base class to set the status information on selection changes
0627: public void columnSelectionChanged(ListSelectionEvent e) {
0628: if (!e.getValueIsAdjusting())
0629: showSelectionStatus();
0630:
0631: super .columnSelectionChanged(e);
0632: }
0633:
0634: // The last row and column that were written in a status message
0635: private int _lastRow = -1, _lastCol = -1;
0636:
0637: // Sends the currently selected cell to the status functor
0638: private void showSelectionStatus() {
0639: int row = getSelectedRow();
0640: int col = getSelectedColumn();
0641: if (row < 0 || col < 0)
0642: return;
0643:
0644: if (row == _lastRow && col == _lastCol)
0645: return;
0646:
0647: _lastRow = row;
0648: _lastCol = col;
0649: Cell cell = getCellIfPresent(row, col);
0650: if (cell != null)
0651: setStatus(cell.toString());
0652: else
0653: setStatus("cell(" + row + "," + col + ")");
0654: }
0655:
0656: // ===========================================
0657: // Spreadsheet Model
0658: // ===========================================
0659:
0660: private class SpreadsheetTableModel extends AbstractTableModel
0661: implements Observer {
0662:
0663: static final long serialVersionUID = -6455541616661139146L;
0664:
0665: // The contents of the model: a set of cells keyed by their address...
0666: private Map<Point, Cell> _cellmap = new HashMap<Point, Cell>();
0667:
0668: // ... and by their names
0669: private Map<String, Cell> _namemap = new HashMap<String, Cell>();
0670:
0671: // The number of rows in the model
0672: private int _numRows;
0673:
0674: // The number of columns in the model
0675: private int _numCols;
0676:
0677: /**
0678: * Builds a SpreadsheetTableModel of the default size (16x16)
0679: */
0680: public SpreadsheetTableModel() {
0681: this (16, 16);
0682: }
0683:
0684: /**
0685: * Builds a SpreadsheetTableModel of the given size
0686: */
0687: public SpreadsheetTableModel(int rows, int cols) {
0688: _numRows = rows;
0689: _numCols = cols;
0690: }
0691:
0692: /**
0693: * Removes all information from the model
0694: */
0695: public void clear() {
0696: _cellmap = new HashMap<Point, Cell>();
0697: _namemap = new HashMap<String, Cell>();
0698: fireTableDataChanged();
0699: }
0700:
0701: /**
0702: * Returns the cell at the given address, or null if no such cell
0703: * exists.
0704: * @throws IndexOutOfBoundsException if the row or column is out of
0705: * bounds
0706: */
0707: public Cell getCellAt(int row, int col)
0708: throws IndexOutOfBoundsException {
0709: if (checkCellAddress(row, col))
0710: return _cellmap.get(new Point(row, col));
0711:
0712: if (row < 0 || row >= _numRows) {
0713: String msg = "Row " + row + " out of range: 0.."
0714: + (_numRows - 1);
0715: throw new IndexOutOfBoundsException(msg);
0716: }
0717:
0718: String msg = "Col " + col + " out of range: 0.."
0719: + (_numCols - 1);
0720: throw new IndexOutOfBoundsException(msg);
0721: }
0722:
0723: /**
0724: * Builds a read-only cell to hold the given value
0725: * @throws IndexOutOfBoundsException if the row or column is out of
0726: * bounds
0727: * @throws IllegalArgumentException if the cell has already been set
0728: */
0729: private <T> Cell setCellAt(Class<T> type, Generator<T> gen,
0730: int row, int col) {
0731: // TODO: edit existing cells, changing their types as necessary
0732:
0733: // @SuppressWarnings
0734: // the Cell returned by is non-generic.
0735: Cell cell = getCellAt(row, col);
0736: if (cell != null) {
0737: throw new IllegalArgumentException(cell
0738: + " has already been set");
0739: }
0740:
0741: if (gen == null)
0742: return null;
0743:
0744: return setCell(new Cell(Spreadsheet.this , type, new Point(
0745: row, col), gen));
0746: }
0747:
0748: /**
0749: * Builds a possibly editable cell to hold the given formula
0750: * @throws IndexOutOfBoundsException if the row or column is out of
0751: * bounds
0752: * @throws IllegalArgumentException if the cell has already been set
0753: */
0754: private Cell setCellAt(String formula, int row, int col,
0755: boolean editable) {
0756: // @SuppressWarnings
0757: // the Cell returned by getCellAt is non-generic.
0758: Cell cell = getCellAt(row, col);
0759: if (cell != null) {
0760: throw new IllegalArgumentException(cell
0761: + " has already been set");
0762: }
0763:
0764: if (formula == null || "".equals(formula))
0765: return null;
0766:
0767: return setCell(new Cell(Spreadsheet.this , new Point(row,
0768: col), formula, editable));
0769: }
0770:
0771: /**
0772: */
0773: private Cell setCell(Cell cell) {
0774: String name = cell.getName();
0775: if (name != null) {
0776: if (_namemap.get(name) != null) {
0777: String err = "Duplicate cell name " + name;
0778: throw new IllegalArgumentException(err);
0779: }
0780:
0781: _namemap.put(name, cell);
0782: }
0783:
0784: cell.addObserver(this );
0785:
0786: Point p = cell.getAddress();
0787: _cellmap.put(p, cell);
0788: fireTableCellUpdated(p.x, p.y);
0789: return cell;
0790: }
0791:
0792: /**
0793: * Returns a (possibly null) cell whose name is given
0794: */
0795: private Cell getCellByName(String name) {
0796: return _namemap.get(name);
0797: }
0798:
0799: /**
0800: * Sets the name of a cell
0801: */
0802: private Cell setCellName(String name, int row, int col) {
0803: if (name != null) {
0804: Cell cell = _namemap.get(name);
0805: if (cell != null) {
0806: String err = "Duplicate cell name " + name;
0807: throw new IllegalArgumentException(err);
0808: }
0809: }
0810:
0811: Cell cell = Spreadsheet.this .getCellAt(row, col);
0812: String oldname = cell.getName();
0813: if (oldname != null)
0814: _namemap.remove(oldname);
0815:
0816: if (name != null)
0817: _namemap.put(name, cell);
0818:
0819: cell.setName(name);
0820:
0821: // TODO: need to notify the cell's observers that the name has changed.
0822: // This may force a change from an Observable/Observer to the creation
0823: // of cell events (CellValueChanged, CellNameChanged, CellFormatChanged)
0824: return cell;
0825:
0826: }
0827:
0828: // NOTE: the Class<T> parm is used by the inferencer to determine the
0829: // type of the object returned.
0830:
0831: // NOTE: If editing of cells is implemented by replacing existing cells
0832: // with new cells, then this method will have to change a little: the
0833: // reference should be served by the spreadsheet and not the cell, so
0834: // that the spreadsheet is free to swap cells in and out
0835:
0836: /**
0837: * Returns a reference to the contents of a given cell
0838: */
0839: public <T> Generator<T> getReference(Class<T> type, int row,
0840: int col) {
0841: Cell cell = getCellAt(row, col);
0842: if (cell == null) {
0843: String msg = "Cell({0},{1}) is not yet defined";
0844: throw new IllegalArgumentException(MessageFormat
0845: .format(msg, new Object[] { row, col }));
0846: }
0847: if (type.isAssignableFrom(cell.getType())) {
0848:
0849: // @SuppressWarnings
0850: // the Cell returned by getCellAt is non-generic, as is the reference
0851: // that it returns here. The Cell class catches ClassCastException when
0852: // it is evaluated, so while this isn't typesafe, the effects are trapped
0853: return cell.getReference();
0854: }
0855:
0856: String err = "Cannot return reference of type {0} from {1}, whose type is {2}";
0857: String msg = MessageFormat.format(err, new Object[] { type,
0858: cell, cell.getType() });
0859: throw new ClassCastException(msg);
0860: }
0861:
0862: // implementation of the EditableByDefault property
0863: private boolean _editableByDefault;
0864:
0865: private void setEditableByDefault(boolean b) {
0866: _editableByDefault = b;
0867: }
0868:
0869: private boolean isEditableByDefault() {
0870: return _editableByDefault;
0871: }
0872:
0873: public void setRowCount(int height) {
0874: int oldHeight = _numRows;
0875: _numRows = height;
0876:
0877: if (oldHeight < height) {
0878: fireTableRowsInserted(oldHeight, height - 1);
0879: } else {
0880: removeCells();
0881: fireTableRowsDeleted(height, oldHeight - 1);
0882: }
0883: }
0884:
0885: public void setColumnCount(int width) {
0886: int oldWidth = _numCols;
0887: _numCols = width;
0888: if (width < oldWidth) {
0889: removeCells();
0890: }
0891:
0892: fireTableStructureChanged();
0893: }
0894:
0895: private void removeCells() {
0896: for (Iterator iter = _cellmap.values().iterator(); iter
0897: .hasNext();) {
0898: Cell cell = (Cell) iter.next();
0899: Point p = cell.getAddress();
0900: if (p.x >= _numRows || p.y >= _numCols) {
0901: iter.remove();
0902:
0903: String name = cell.getName();
0904: if (name != null)
0905: _namemap.remove(cell.getName());
0906:
0907: cell.unlink();
0908: }
0909: }
0910: }
0911:
0912: // - - - - - - - - - - -
0913: // TableModel interface
0914: // - - - - - - - - - - -
0915:
0916: public int getRowCount() {
0917: return _numRows;
0918: }
0919:
0920: public int getColumnCount() {
0921: return _numCols;
0922: }
0923:
0924: public Object getValueAt(int row, int col) {
0925: if (!checkCellAddress(row, col))
0926: return Cell.REFERENCE_ERR;
0927:
0928: Cell cell = _cellmap.get(new Point(row, col));
0929: if (cell == null)
0930: return null;
0931:
0932: Object obj = cell.getValue();
0933: return cell.isUndefined() ? "" : cell.isValid() ? obj
0934: : cell.getErrorMsg();
0935: }
0936:
0937: // NOTE: I hook into this method for undo/redo notifications because only
0938: // user edits of cell values flows through this method -- programatic updates
0939: // to cells (including updates caused by changes to prerequisite cells) don't
0940: // flow through the TableModel interface. This is temporary: the Cell class
0941: // is likely to grow support for UndoableEdits, and start a chain that ends
0942: // up passing them out to the enclosing application
0943:
0944: public void setValueAt(Object value, int row, int col) {
0945: Cell cell = getCellAt(row, col);
0946: if (cell != null) {
0947: cell.setValue(value);
0948: } else if (value != null) {
0949: setCellAt(value.toString(), row, col, true);
0950: }
0951:
0952: /*temp*/fireSpreadsheetUpdated();
0953: }
0954:
0955: public boolean isCellEditable(int row, int col) {
0956: Cell cell = getCellAt(row, col);
0957: if (cell != null)
0958: return cell.isEditable();
0959:
0960: return _editableByDefault;
0961: }
0962:
0963: // - - - - - - - - - - - -
0964: // Observer Interface
0965: // - - - - - - - - - - - -
0966:
0967: public void update(Observable observable, Object object) {
0968: Cell cell = (Cell) observable;
0969: Point addr = cell.getAddress();
0970: fireTableCellUpdated(addr.x, addr.y);
0971: }
0972:
0973: // - - - - - - - - - - - -
0974: // implementation details
0975: // - - - - - - - - - - - -
0976:
0977: private boolean checkCellAddress(int row, int col) {
0978: return row >= 0 && row < _numRows && col >= 0
0979: && col < _numCols;
0980: }
0981: }
0982:
0983: // ===========================================
0984: // Spreadsheet Parser
0985: // ===========================================
0986:
0987: class Parser extends JFXGParser {
0988: private Cell _crntCell;
0989:
0990: private Parser() {
0991: super ();
0992: }
0993:
0994: public Generator parseGenerator(Cell cell, String str)
0995: throws ParseException {
0996: _crntCell = cell;
0997: try {
0998: return super .parseGenerator(str);
0999: } finally {
1000: _crntCell = null;
1001: }
1002:
1003: }
1004:
1005: /**
1006: * Suppresses the binding method: inside a spreadsheet, 'this' always refers to
1007: * the spreadsheet itself.
1008: */
1009: public void bindThis(Object this Binding) {
1010: super .bindThis(Spreadsheet.this );
1011: }
1012:
1013: /**
1014: * Overrides the base parser class mechanism for resolving methods, in order to
1015: * protect certain methods from abuse.
1016: */
1017: protected FunctorRef resolveMethodName(FunctorRef prefix,
1018: String name, FunctorRef[] args) throws ParseException {
1019: // If the prefix is not a constant reference to this spreadsheet, defer to the superclass
1020: if (prefix.getReferenceType() != FunctorRef.CONSTANT)
1021: return super .resolveMethodName(prefix, name, args);
1022:
1023: // It has to be a CONSTANT to get here, so the cast is known safe
1024: if (((GeneratorRef) prefix).getFunctor().gen() != Spreadsheet.this )
1025: return super .resolveMethodName(prefix, name, args);
1026:
1027: // We've established that the prefix is a constant reference to this spreadsheet.
1028: return super .resolveMethodName(prefix, name, args);
1029: }
1030:
1031: /**
1032: * Implements the <i>row</i>, and <i>col</i> keywords.
1033: */
1034: protected FunctorRef reservedWord(String word)
1035: throws ParseException {
1036: if (word.equals("row")) {
1037: return new GeneratorRef(new Constant(_crntCell
1038: .getAddress().x), Integer.class);
1039: }
1040:
1041: if (word.equals("col")) {
1042: return new GeneratorRef(new Constant(_crntCell
1043: .getAddress().y), Integer.class);
1044: }
1045:
1046: return super .reservedWord(word);
1047: }
1048:
1049: /**
1050: * Implements the <i>cell</i> keyword.
1051: */
1052: protected FunctorRef reservedFunction(String name,
1053: FunctorRef[] args) throws ParseException {
1054: if (name.equals("cell")) {
1055: if (args.length == 1 && args[0] instanceof GeneratorRef
1056: && args[0].getReturnType().equals(String.class)) {
1057: String refname = (String) ((GeneratorRef) args[0])
1058: .getFunctor().gen();
1059: Cell cell = getCellByName(refname);
1060: if (cell == null) {
1061: throw new ParseException("Unknown Cell Name: "
1062: + refname);
1063: }
1064:
1065: return new GeneratorRef(cell.getReference(), cell
1066: .getType());
1067: }
1068:
1069: if (args.length != 2
1070: || !(args[0] instanceof GeneratorRef)
1071: || !(args[1] instanceof GeneratorRef)
1072: || !(args[0].getReturnType()
1073: .equals(Integer.class))
1074: || !(args[1].getReturnType()
1075: .equals(Integer.class)))
1076: throw new ParseException(
1077: "Cell Reference requires row, col arguments");
1078:
1079: int row = ((Integer) ((GeneratorRef) args[0])
1080: .getFunctor().gen()).intValue();
1081: int col = ((Integer) ((GeneratorRef) args[1])
1082: .getFunctor().gen()).intValue();
1083: Cell cell = getCellAt(row, col);
1084: return new GeneratorRef(cell.getReference(), cell
1085: .getType());
1086: }
1087:
1088: return super .reservedFunction(name, args);
1089: }
1090: }
1091:
1092: // ===========================================
1093: // SpreadsheetWriter
1094: // ===========================================
1095:
1096: public class Writer {
1097: private TransformerHandler _handler;
1098: private Transformer _xformer;
1099: private Set<Cell> _cellsWritten;
1100:
1101: public Writer() throws IOException {
1102: try {
1103: SAXTransformerFactory tf = (SAXTransformerFactory) SAXTransformerFactory
1104: .newInstance();
1105: _handler = tf.newTransformerHandler();
1106:
1107: Transformer xformer = _handler.getTransformer();
1108: xformer.setOutputProperty(OutputKeys.ENCODING,
1109: "ISO-8859-1");
1110:
1111: // TODO: link in the DTD, ideally as a resource loaded from the jar,
1112: // I don't want to have to post the DTD on the site, as that will break
1113: // code if/when the project is moved.
1114:
1115: // xformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM,"doc/hacksheet.dtd");
1116: xformer.setOutputProperty(OutputKeys.INDENT, "yes");
1117: } catch (TransformerConfigurationException x) {
1118: IOException iox = new IOException(x.getMessage());
1119: iox.initCause(x);
1120: throw iox;
1121: }
1122: }
1123:
1124: public void writeSpreadsheet(OutputStream os)
1125: throws IOException {
1126: try {
1127: // Set up all the JAXP overhead
1128: StreamResult stream = new StreamResult(os);
1129: _handler.setResult(stream);
1130: _handler.startDocument();
1131:
1132: AttributesImpl atts = new AttributesImpl();
1133: int rows = getRowCount();
1134: int cols = getColumnCount();
1135:
1136: // Start the spreadsheet document
1137: //
1138: // <hacksheet vers="%d" rows="%d" cols="%d" defaultType="%s" defaultValue="%s">
1139: atts.clear();
1140: atts.addAttribute("", "", "vers", "", "0.1.0");
1141: atts.addAttribute("", "", "rows", "", String
1142: .valueOf(rows));
1143: atts.addAttribute("", "", "cols", "", String
1144: .valueOf(cols));
1145:
1146: Class defaultType = getDefaultCellType();
1147: if (!defaultType.equals(Integer.class)) {
1148: atts.addAttribute("", "", "defaultType", "",
1149: defaultType.getName());
1150: }
1151:
1152: Object defaultValue = getDefaultCellValue();
1153: if (!defaultValue.equals(0)) {
1154: atts.addAttribute("", "", "defaultValue", "",
1155: getDefaultCellValue().toString());
1156: }
1157:
1158: _handler.startElement("", "", "hacksheet", atts);
1159:
1160: _cellsWritten = new HashSet<Cell>(_model._cellmap
1161: .values().size() * 4 / 3);
1162: writeCells(_model._cellmap.values().iterator());
1163:
1164: _handler.endElement("", "", "hacksheet");
1165: _handler.endDocument();
1166: } catch (SAXException x) {
1167: IOException iox = new IOException(x.getMessage());
1168: iox.initCause(x);
1169: throw iox;
1170: }
1171: }
1172:
1173: private void writeCells(Iterator<Cell> cells)
1174: throws SAXException {
1175: while (cells.hasNext()) {
1176: Cell cell = cells.next();
1177: if (!_cellsWritten.contains(cell)) {
1178: writeCells(cell.dependsOn());
1179: writeCell(cell);
1180: }
1181: }
1182: }
1183:
1184: private void writeCell(Cell cell) throws SAXException {
1185: AttributesImpl atts = new AttributesImpl();
1186:
1187: String name = cell.getName();
1188: if (name != null && name.trim().length() != 0)
1189: atts.addAttribute("", "", "id", "", cell.getName());
1190:
1191: Point p = cell.getAddress();
1192: atts.addAttribute("", "", "row", "", String.valueOf(p.x));
1193: atts.addAttribute("", "", "col", "", String.valueOf(p.y));
1194: atts.addAttribute("", "", "type", "", cell.getType()
1195: .getName());
1196: atts.addAttribute("", "", "editable", "", String
1197: .valueOf(cell.isEditable()));
1198:
1199: String formula = cell.getFormula();
1200: _handler.startElement("", "", "cell", atts);
1201:
1202: atts.clear();
1203: _handler.startElement("", "", "formula", atts);
1204: _handler.characters(formula.toCharArray(), 0, formula
1205: .length());
1206: _handler.endElement("", "", "formula");
1207: _handler.endElement("", "", "cell");
1208:
1209: _cellsWritten.add(cell);
1210: }
1211: }
1212:
1213: // ===========================================
1214: // SpreadsheetReader
1215: // ===========================================
1216:
1217: public class Reader extends DefaultHandler {
1218: StringBuffer buf = new StringBuffer();
1219:
1220: public void readSpreadsheet(InputStream is) throws IOException {
1221: try {
1222: createParser().parse(new InputSource(is));
1223: } catch (SAXException x) {
1224: IOException iox = new IOException(x.getMessage());
1225: iox.initCause(x);
1226: throw iox;
1227: } catch (ParserConfigurationException x) {
1228: IOException iox = new IOException(x.getMessage());
1229: iox.initCause(x);
1230: throw iox;
1231: }
1232: }
1233:
1234: private Cell _crntCell;
1235:
1236: public void startElement(String nsURI, String localname,
1237: String qname, Attributes attr) throws SAXException {
1238: if (qname.equals("hacksheet")) {
1239: String vers = attr.getValue("vers");
1240: // TODO: write a version check
1241:
1242: int rows = Integer.parseInt(attr.getValue("rows"));
1243: int cols = Integer.parseInt(attr.getValue("cols"));
1244: SpreadsheetTableModel model = new SpreadsheetTableModel(
1245: rows, cols);
1246: SpreadsheetTableModel oldModel = _model;
1247: setModel(model);
1248: } else if (qname.equals("cell")) {
1249: int row = Integer.parseInt(attr.getValue("row"));
1250: int col = Integer.parseInt(attr.getValue("col"));
1251: _crntCell = new Cell(Spreadsheet.this , row, col);
1252: _crntCell.setEditable(Boolean.valueOf(attr
1253: .getValue("editable")));
1254: String name = attr.getValue("id");
1255: if (name != null) {
1256: _crntCell.setName(name);
1257: }
1258: } else if (qname.equals("formula")) {
1259: buf.delete(0, buf.length());
1260: } else
1261: throw new SAXException("unknown tag \"" + qname + "\"");
1262: }
1263:
1264: public void endElement(String nsURI, String localname,
1265: String qname) throws SAXException {
1266: if (qname.equals("hacksheet")) {
1267: } else if (qname.equals("cell")) {
1268: _model.setCell(_crntCell);
1269: } else if (qname.equals("formula")) {
1270: _crntCell.setFormula(buf.toString());
1271: buf.delete(0, buf.length());
1272: } else
1273: throw new SAXException("unknown tag \"" + qname + "\"");
1274: }
1275:
1276: public void characters(char[] ch, int start, int ln)
1277: throws SAXException {
1278: for (int i = 0; i < ln; ++i) {
1279: buf.append(ch[start + i]);
1280: }
1281: }
1282:
1283: public XMLReader createParser() throws SAXException,
1284: ParserConfigurationException {
1285: SAXParserFactory spf = SAXParserFactory.newInstance();
1286: spf.setValidating(false);
1287:
1288: SAXParser saxParser = spf.newSAXParser();
1289: XMLReader xmlrd = saxParser.getXMLReader();
1290: xmlrd.setContentHandler(this );
1291: xmlrd.setErrorHandler(this );
1292: return xmlrd;
1293: }
1294: }
1295: }
1296:
1297: // ===========================================
1298: // RowHeader
1299: // ===========================================
1300:
1301: class RowHeader extends JComponent {
1302: static final long serialVersionUID = -1375303876648436931L;
1303:
1304: private JTable _table;
1305: private TableCellRenderer _renderer;
1306: private JTableHeader _header;
1307: private CellRendererPane _rendererPane;
1308: private Font _headerFont;
1309:
1310: public RowHeader(JTable table) {
1311: _table = table;
1312: _header = table.getTableHeader();
1313: _renderer = _header.getDefaultRenderer();
1314: _rendererPane = new CellRendererPane();
1315: add(_rendererPane);
1316:
1317: Component rendererComponent = _renderer
1318: .getTableCellRendererComponent(_table, "0", false,
1319: false, 0, -1);
1320:
1321: _headerFont = rendererComponent.getFont();
1322:
1323: setFont(_headerFont);
1324: setBackground(rendererComponent.getBackground());
1325: setForeground(rendererComponent.getForeground());
1326: // setRowCount(table.getRowCount());
1327: }
1328:
1329: public TableCellRenderer getRenderer() {
1330: return _renderer;
1331: }
1332:
1333: public void setRenderer(TableCellRenderer renderer) {
1334: _renderer = renderer;
1335: }
1336:
1337: public void setRowCount(int count) {
1338: resize(getPreferredSize());
1339: }
1340:
1341: public Dimension getPreferredSize() {
1342: Border border = (Border) UIManager.getDefaults().get(
1343: "TableHeader.cellBorder");
1344: Insets insets = border.getBorderInsets(_header);
1345: FontMetrics metrics = getFontMetrics(_headerFont);
1346: Dimension dim = new Dimension(metrics.stringWidth("99999")
1347: + insets.right + insets.left, _table.getRowHeight()
1348: * _table.getRowCount());
1349: return dim;
1350: }
1351:
1352: protected void paintComponent(Graphics g) {
1353:
1354: // TODO: this is naive: we should take the clipping region into account and
1355: // only paint the visible rows
1356:
1357: Rectangle cellRect = new Rectangle(0, 0, getWidth(), _table
1358: .getRowHeight(0));
1359: int rowMargin = _header.getColumnModel().getColumnMargin() - 1;
1360: for (int i = 0; i < _table.getRowCount(); ++i) {
1361: int rowHeight = _table.getRowHeight(i);
1362: cellRect.height = rowHeight - rowMargin;
1363: paintCell(g, cellRect, i);
1364: cellRect.y += rowHeight;
1365: }
1366: }
1367:
1368: private void paintCell(Graphics g, Rectangle cellRect, int rowIndex) {
1369: Component component = _renderer.getTableCellRendererComponent(
1370: _table, rowIndex, false, false, rowIndex, -1);
1371:
1372: _rendererPane.paintComponent(g, component, this , cellRect.x,
1373: cellRect.y, cellRect.width, cellRect.height, true);
1374: }
1375: }
|