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 }