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 */
021package csheets.ui.sheet;
022
023import java.awt.Graphics;
024import java.awt.event.ActionEvent;
025import java.awt.event.KeyEvent;
026import java.beans.PropertyChangeEvent;
027import java.beans.PropertyChangeListener;
028import java.util.Enumeration;
029import java.util.LinkedList;
030
031import javax.swing.AbstractAction;
032import javax.swing.Action;
033import javax.swing.ActionMap;
034import javax.swing.KeyStroke;
035import javax.swing.TransferHandler;
036import javax.swing.table.TableColumn;
037import javax.swing.table.TableModel;
038
039import csheets.core.Address;
040import csheets.core.Cell;
041import csheets.core.Spreadsheet;
042import csheets.ext.style.StylableSpreadsheet;
043import csheets.ext.style.StyleExtension;
044import csheets.ui.ctrl.SelectionEvent;
045import csheets.ui.ctrl.SelectionListener;
046import csheets.ui.ctrl.UIController;
047import csheets.ui.ext.TableDecorator;
048import csheets.ui.ext.UIExtension;
049import csheets.ui.grid.Grid;
050
051/**
052 * A customized JTable component, used to visualize a spreadsheet.
053 * @author Einar Pehrson
054 */
055@SuppressWarnings("serial")
056public 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}