001    /*
002     * Copyright (c) 2005 Einar Pehrson <einar@pehrson.nu>.
003     *
004     * This file is part of
005     * CleanSheets - a spreadsheet application for the Java platform.
006     *
007     * CleanSheets is free software; you can redistribute it and/or modify
008     * it under the terms of the GNU General Public License as published by
009     * the Free Software Foundation; either version 2 of the License, or
010     * (at your option) any later version.
011     *
012     * CleanSheets is distributed in the hope that it will be useful,
013     * but WITHOUT ANY WARRANTY; without even the implied warranty of
014     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
015     * GNU General Public License for more details.
016     *
017     * You should have received a copy of the GNU General Public License
018     * along with CleanSheets; if not, write to the Free Software
019     * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
020     */
021    package csheets.ui.sheet;
022    
023    import java.awt.Graphics;
024    import java.awt.event.ActionEvent;
025    import java.awt.event.KeyEvent;
026    import java.beans.PropertyChangeEvent;
027    import java.beans.PropertyChangeListener;
028    import java.util.Enumeration;
029    import java.util.LinkedList;
030    
031    import javax.swing.AbstractAction;
032    import javax.swing.Action;
033    import javax.swing.ActionMap;
034    import javax.swing.KeyStroke;
035    import javax.swing.TransferHandler;
036    import javax.swing.table.TableColumn;
037    import javax.swing.table.TableModel;
038    
039    import csheets.core.Address;
040    import csheets.core.Cell;
041    import csheets.core.Spreadsheet;
042    import csheets.ext.style.StylableSpreadsheet;
043    import csheets.ext.style.StyleExtension;
044    import csheets.ui.ctrl.SelectionEvent;
045    import csheets.ui.ctrl.SelectionListener;
046    import csheets.ui.ctrl.UIController;
047    import csheets.ui.ext.TableDecorator;
048    import csheets.ui.ext.UIExtension;
049    import csheets.ui.grid.Grid;
050    
051    /**
052     * A customized JTable component, used to visualize a spreadsheet.
053     * @author Einar Pehrson
054     */
055    @SuppressWarnings("serial")
056    public class SpreadsheetTable extends Grid implements SelectionListener {
057    
058            /** The action command used for the action */
059            public static final String CLEAR_SELECTION_COMMAND = "Clear the content of the selected cells";
060    
061            /** The spreadsheet that is displayed by the table */
062            private Spreadsheet spreadsheet;
063    
064            /** The user interface controller */
065            private UIController uiController;
066    
067            /** The table decorators invoked when painting the table */
068            private java.util.List<TableDecorator> decorators
069                    = new LinkedList<TableDecorator>();
070    
071            /** The column width tracker */
072            private PropertyChangeListener columnWidthTracker = new ColumnWidthTracker();
073    
074            /**
075             * Creates a spreadsheet table for the given spreadsheet.
076             * @param spreadsheet the spreadsheet to display in the table
077             * @param uiController the user interface controller
078             */
079            public SpreadsheetTable(Spreadsheet spreadsheet, UIController uiController) {
080                    this(new SpreadsheetTableModel(spreadsheet, uiController), uiController);
081            }
082    
083            /**
084             * Creates a spreadsheet table for the given spreadsheet table model.
085             * @param tableModel the spreadsheet table model to display in the table
086             * @param uiController the user interface controller
087             */
088            public SpreadsheetTable(SpreadsheetTableModel tableModel, UIController uiController) {
089                    super(null);
090    
091                    // Stores members
092                    this.uiController = uiController;
093                    uiController.addSelectionListener(this);
094    
095                    // Configures cell rendering and editing
096                    setDefaultRenderer(Cell.class, new CellRenderer(uiController));
097                    setDefaultEditor(Cell.class, new CellEditor(uiController));
098                    setDragEnabled(true);
099                    setTransferHandler(uiController.getCellTransferHandler());
100    
101                    // Configures cell editing actions
102                    ActionMap actionMap = getActionMap();
103                    actionMap.put(TransferHandler.getCutAction().getValue(Action.NAME),
104                            TransferHandler.getCutAction());
105                    actionMap.put(TransferHandler.getCopyAction().getValue(Action.NAME),
106                            TransferHandler.getCopyAction());
107                    actionMap.put(TransferHandler.getPasteAction().getValue(Action.NAME),
108                            TransferHandler.getPasteAction());
109                    actionMap.put(CLEAR_SELECTION_COMMAND, new ClearSelectionAction());
110                    getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0),
111                            CLEAR_SELECTION_COMMAND);
112    
113                    // Fetches decorators
114                    for (UIExtension extension : uiController.getExtensions()) {
115                            TableDecorator decorator = extension.getTableDecorator();
116                            if (decorator != null)
117                                    decorators.add(decorator);
118                    }
119    
120                    // Updates model
121                    setModel(tableModel);
122            }
123    
124    /*
125     * DATA
126     */
127    
128            /**
129             * Returns the spreadsheet that the table displays.
130             * @return the spreadsheet that the table displays.
131             */
132            public Spreadsheet getSpreadsheet() {
133                    return spreadsheet;
134            }
135    
136            /**
137             * Sets the spreadsheet that is displayed by the table.
138             * @param spreadsheet the spreadsheet that is displayed by the table
139             */
140            public void setSpreadsheet(Spreadsheet spreadsheet) {
141                    setModel(new SpreadsheetTableModel(spreadsheet, uiController));
142            }
143    
144            /**
145             * Sets the data model of the table. Overridden to only accept instances
146             * of the <code>SpreadsheetTableModel</code> class.
147             * @param dataModel the new data source for this table, must be a <code>SpreadsheetTableModel</code>
148             */
149            public void setModel(TableModel dataModel) {
150                    if (!(dataModel instanceof SpreadsheetTableModel))
151                            return;
152    
153                    // Updates model
154                    this.spreadsheet = ((SpreadsheetTableModel)dataModel).getSpreadsheet();
155                    super.setModel(dataModel);
156    
157                    // Restores column widths and row heights
158                    StylableSpreadsheet styleableSpreadsheet = (StylableSpreadsheet)
159                            spreadsheet.getExtension(StyleExtension.NAME);
160                    for (int column = 0; column < spreadsheet.getColumnCount(); column++) {
161                            int columnWidth = styleableSpreadsheet.getColumnWidth(column);
162                            if (columnWidth != -1)
163                                    columnModel.getColumn(column).setPreferredWidth(columnWidth);
164                    }
165                    for (int row = 0; row < spreadsheet.getRowCount(); row++) {
166                            int rowHeight = styleableSpreadsheet.getRowHeight(row);
167                            if (rowHeight != -1)
168                                    super.setRowHeight(row, rowHeight);
169                    }
170    
171                    // Adds column width listener
172                    Enumeration<TableColumn> columns = columnModel.getColumns();
173                    while (columns.hasMoreElements())
174                            columns.nextElement().addPropertyChangeListener(columnWidthTracker);
175            }
176    
177    /*
178     * SELECTION
179     */
180    
181            /**
182             * Returns the active cell of the spreadsheet table.
183             * @return the active cell of the spreadsheet table
184             */
185            public Cell getSelectedCell() {
186                    int activeColumn = getColumnModel().getSelectionModel().getAnchorSelectionIndex();
187                    int activeRow = getSelectionModel().getAnchorSelectionIndex();
188                    return spreadsheet.getCell(new Address(activeColumn, activeRow));
189            }
190    
191            /**
192             * Returns the currently selected cells in the spreadsheet table.
193             * @return a two-dimensional array of the the currently selected cells in the spreadsheet table
194             */
195            public Cell[][] getSelectedCells() {
196                    int[] rows = getSelectedRows();
197                    int[] columns = getSelectedColumns();
198                    Cell[][] range = new Cell[rows.length][columns.length];
199                    for (int row = 0; row < range.length; row++)
200                            for (int column = 0; column < range[row].length; column++)
201                                    range[row][column] = spreadsheet.getCell(columns[column], rows[row]);
202                    return range;
203            }
204    
205            /**
206             * Clears the currently selected cells in the table.
207             */
208            public void clearSelectedCells() {
209                    for (Cell[] row : getSelectedCells())
210                            for (Cell cell : row)
211                                    cell.clear();
212            }
213    
214            /**
215             * Changes the current selection in the table. Overridden to update the
216             * user interface controller as well.
217             * @param row the row that was selected
218             * @param column the column that was selected
219             * @param toggle whether the selection should be toggled
220             * @param extend whether the selection should be extended
221             */
222            public void changeSelection(int row, int column, boolean toggle, boolean extend) {
223                    super.changeSelection(row, column, toggle, extend);
224                    if (!extend)
225                            uiController.setActiveCell(getSelectedCell());
226            }
227    
228            /**
229             * Selects all cells in the spreadsheet table.
230             */
231            public void selectAll() {
232                    super.changeSelection(0, 0, false, false);
233                    changeSelection(
234                            spreadsheet.getRowCount(),
235                            spreadsheet.getColumnCount(), false, true);
236                    uiController.setActiveCell(getSelectedCell());
237            }
238    
239            /**
240             * Updates the selection in the table when the active cell is changed.
241             * @param event the selection event that was fired
242             */
243            public void selectionChanged(SelectionEvent event) {
244                    if (spreadsheet == event.getSpreadsheet() && event.isCellChanged()) {
245                            int activeColumn = getColumnModel().getSelectionModel().getAnchorSelectionIndex();
246                            int activeRow = getSelectionModel().getAnchorSelectionIndex();
247                            Address address = event.getCell().getAddress();
248                            if (event.getPreviousCell() == null || (address.getColumn()
249                                    != activeColumn || address.getRow() != activeRow)) {
250                                    changeSelection(address.getRow(), address.getColumn(), false, false);
251                                    requestFocus();
252                            }
253                    }
254            }
255    
256    /*
257     * DECORATION
258     */
259    
260            /**
261             * Overridden to delegate painting to decorators.
262             * @param g the Graphics object to protect
263             */
264            protected void paintComponent(Graphics g) {
265                    super.paintComponent(g);
266    
267                    // Invokes decorators
268                    for (TableDecorator decorator : decorators)
269                            if (decorator.isEnabled())
270                                    decorator.decorate(g, this);
271            }
272    
273    /*
274     * HEADERS
275     */
276    
277            /**
278             * Sets the height for row to rowHeight, revalidates, and repaints. The height of the cells in this row will be equal to the row height minus the row margin. 
279             * @param row - the row whose height is being changed
280             * @param rowHeight - new row height, in pixels 
281             * @throws IllegalArgumentException if rowHeight is less than 1
282             */
283            public void setRowHeight(int row, int rowHeight) {
284                    super.setRowHeight(row, rowHeight);
285                    uiController.setWorkbookModified(spreadsheet.getWorkbook());
286                    StylableSpreadsheet styleableSpreadsheet = (StylableSpreadsheet)
287                            spreadsheet.getExtension(StyleExtension.NAME);
288                    styleableSpreadsheet.setRowHeight(row, rowHeight);
289            }
290    
291            /**
292             * A listener that forwards column width changes to the style extension.
293             */
294            private class ColumnWidthTracker implements PropertyChangeListener {
295    
296                    /**
297                     * Creates a new column width tracker.
298                     */
299                    public ColumnWidthTracker() {}
300    
301                    /**
302                     * Stores the width of the column that was resized.
303                     * @param event the event that was fired
304                     */
305                    public void propertyChange(PropertyChangeEvent event) {
306                            if (event.getPropertyName().equals("width")) {
307                                    TableColumn source = (TableColumn)event.getSource();
308                                    StylableSpreadsheet styleableSpreadsheet = (StylableSpreadsheet)
309                                            spreadsheet.getExtension(StyleExtension.NAME);
310                                    if (styleableSpreadsheet.getColumnWidth(source.getModelIndex())
311                                                    != source.getWidth()) {
312                                            styleableSpreadsheet.setColumnWidth(
313                                                    source.getModelIndex(), source.getWidth());
314                                            uiController.setWorkbookModified(spreadsheet.getWorkbook());
315                                    }
316                            }
317                    }
318            }
319    
320    /*
321     * ACTIONS
322     */
323    
324            /**
325             * An action for clearing the content of the selected cells, without
326             * invoking the editor.
327             * @author Einar Pehrson
328             */
329            @SuppressWarnings("serial")
330            protected class ClearSelectionAction extends AbstractAction {
331    
332                    /**
333                     * Creates a selection clearing action.
334                     */
335                    public ClearSelectionAction() {
336                            // Configures action
337                            putValue(NAME, CLEAR_SELECTION_COMMAND);
338                            putValue(SHORT_DESCRIPTION, CLEAR_SELECTION_COMMAND);
339                            putValue(ACTION_COMMAND_KEY, CLEAR_SELECTION_COMMAND);
340                    }
341    
342                    public void actionPerformed(ActionEvent event) {
343                            clearSelectedCells();
344                    }
345            }
346    }