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}