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.Component; 024import java.awt.event.ActionEvent; 025import java.awt.event.KeyEvent; 026import java.awt.event.MouseEvent; 027import java.util.EventObject; 028 029import javax.swing.AbstractAction; 030import javax.swing.JOptionPane; 031import javax.swing.JTable; 032import javax.swing.JTextField; 033import javax.swing.KeyStroke; 034import javax.swing.SwingUtilities; 035import javax.swing.event.CellEditorListener; 036import javax.swing.event.ChangeEvent; 037import javax.swing.table.TableCellEditor; 038import javax.swing.text.Document; 039import javax.swing.text.PlainDocument; 040 041import csheets.core.Address; 042import csheets.core.Cell; 043import csheets.core.formula.compiler.FormulaCompilationException; 044import csheets.core.formula.lang.UnknownElementException; 045import csheets.ui.ctrl.SelectionEvent; 046import csheets.ui.ctrl.SelectionListener; 047import csheets.ui.ctrl.UIController; 048 049/** 050 * The table editor used for editing cells in a spreadsheet. 051 * @author Einar Pehrson 052 */ 053@SuppressWarnings("serial") 054public class CellEditor extends JTextField implements TableCellEditor, SelectionListener { 055 056 /** The required number of mouse clicks before editing starts */ 057 public static final int CLICK_COUNT_TO_START = 2; 058 059 /** The action command used for the cancel action */ 060 public static final String CANCEL_COMMAND = "Cancel editing"; 061 062 /** The shared document used to store cell contents */ 063 private static Document document = new PlainDocument(); 064 065 /** The cell that is being edited */ 066 private Cell cell; 067 068 /** Whether the next edit should keep the content of the cell */ 069 private boolean resumeOnNextEdit = false; 070 071 /** The change event that is fired */ 072 private ChangeEvent changeEvent = new ChangeEvent(this); 073 074 /** The user interface controller */ 075 private UIController uiController; 076 077 /** 078 * Creates a new cell editor. 079 * @param uiController the user interface controller 080 */ 081 public CellEditor(UIController uiController) { 082 // Stores members 083 this.uiController = uiController; 084 uiController.addSelectionListener(this); 085 setDocument(document); 086 087 // Applies actions 088 setAction(new StopAction(0, 1)); 089 getActionMap().put(CANCEL_COMMAND, new CancelAction()); 090 getActionMap().put("Stop and move up", new StopAction(0, -1)); 091 getActionMap().put("Stop and move down", new StopAction(0, 1)); 092 getActionMap().put("Stop and move left", new StopAction(-1, 0)); 093 getActionMap().put("Stop and move right", new StopAction(1, 0)); 094 getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), CANCEL_COMMAND); 095 getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "Stop and move up"); 096 getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "Stop and move down"); 097 getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, KeyEvent.SHIFT_MASK), "Stop and move left"); 098 getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), "Stop and move right"); 099 } 100 101 /** 102 * Stops editing and updates the cell's content. 103 * @return true if a change was made, and unless an erroneous formula was entered 104 */ 105 public boolean stopCellEditing() { 106 String content = getText(); 107 if (cell != null && content != null) { 108 // Halts if nothing was changed 109 if (content.equals(cell.getContent())) { 110 cancelCellEditing(); 111 return false; 112 } 113 114 // Updates cell content (and parses formula) 115 try { 116 cell.setContent(content); 117 } catch (FormulaCompilationException e) { 118 // Retrieves correct message 119 String message; 120 if (e.getCause() instanceof antlr.TokenStreamRecognitionException) 121 message = "The parser responded: " + 122 ((antlr.TokenStreamRecognitionException)e.getCause()).recog.getMessage(); 123 else if (e instanceof UnknownElementException) 124 message = "The parser recognized the formula, but a language" 125 + " element (" + ((UnknownElementException)e).getIdentifier() 126 + ") could not be created."; 127 else 128 message = e.getMessage(); 129 130 // Finds the window that contains the editor 131 Component parent = SwingUtilities.getWindowAncestor(this); 132 if (parent == null) 133 parent = this; 134 135 // Inform user of erroneous syntax 136 JOptionPane.showMessageDialog( 137 parent, 138 "The entered formula could not be compiled\n" 139 + message, 140 "Formula compilation error", 141 JOptionPane.ERROR_MESSAGE 142 ); 143 return false; 144 } 145 } 146 147 fireEditingStopped(); 148 return true; 149 } 150 151 /** 152 * Returns the cell that is (or was) being edited. 153 * @return the cell that is (or was) being edited 154 */ 155 public Cell getCellEditorValue() { 156 return cell; 157 } 158 159 /** 160 * Checks if the given event should cause editing to be resumed. 161 * @param event the event that was fired 162 * @return true unless the click-count of a mouse event is too low 163 */ 164 public boolean isCellEditable(EventObject event) { 165 // Checks whether the event should cause editing to be resumed 166 resumeOnNextEdit = event instanceof MouseEvent 167 || (event instanceof ActionEvent && 168 ((ActionEvent)event).getActionCommand().equals( 169 SpreadsheetTable.RESUME_EDIT_COMMAND)); 170 171 // Checks whether editing should start 172 if (event instanceof MouseEvent) 173 return ((MouseEvent)event).getClickCount() >= CLICK_COUNT_TO_START; 174 else 175 return true; 176 } 177 178 /** 179 * Returns true if the given event should cause the cell to be selected. 180 * @param event the event that was fired 181 * @return true 182 */ 183 public boolean shouldSelectCell(EventObject event) { 184 return true; 185 } 186 187 /** 188 * Invoked when editing is cancelled. Simply fires an event. 189 */ 190 public void cancelCellEditing() { 191 fireEditingCanceled(); 192 } 193 194 /** 195 * Stores the given cell in the editor. Depnding on if editing should 196 * be resumed, the text displayed in the editor is either the cell's 197 * content or an empty string. 198 * @param table the table in which the cell is located 199 * @param value the cell to edit 200 * @param selected whether the cell is selected 201 * @param row the row in which the cell is located 202 * @param column the column in which the cell is located 203 */ 204 public Component getTableCellEditorComponent(JTable table, Object value, 205 boolean selected, int row, int column) { 206 if (value != null && value instanceof Cell) { 207 cell = (Cell)value; 208 if (resumeOnNextEdit) 209 setText(((Cell)value).getContent()); 210 else 211 setText(""); 212 } 213 return this; 214 } 215 216 /** 217 * Updates the text field with the content of the new active cell. 218 * @param event the selection event that was fired 219 */ 220 public void selectionChanged(SelectionEvent event) { 221 cell = event.getCell(); 222 if (cell != null) 223 setText(cell.getContent()); 224 else 225 setText(""); 226 } 227 228 /** 229 * Adds a <code>CellEditorListener</code> to the listener list. 230 * @param listener the new listener to be added 231 */ 232 public void addCellEditorListener(CellEditorListener listener) { 233 listenerList.add(CellEditorListener.class, listener); 234 } 235 236 /** 237 * Removes a <code>CellEditorListener</code> from the listener list. 238 * @param listener the listener to be removed 239 */ 240 public void removeCellEditorListener(CellEditorListener listener) { 241 listenerList.remove(CellEditorListener.class, listener); 242 } 243 244 /** 245 * Returns an array of all the <code>CellEditorListener</code>s added. 246 * @return all of the <code>CellEditorListener</code>s added 247 */ 248 public CellEditorListener[] getCellEditorListeners() { 249 return (CellEditorListener[])listenerList.getListeners(CellEditorListener.class); 250 } 251 252 /** 253 * Notifies all listeners that editing was stopped. 254 */ 255 private void fireEditingStopped() { 256 Object[] listeners = listenerList.getListenerList(); 257 for (int i = listeners.length-2; i>=0; i-=2) { 258 if (listeners[i] == CellEditorListener.class) 259 ((CellEditorListener)listeners[i+1]).editingStopped(changeEvent); 260 } 261 } 262 263 /** 264 * Notifies all listeners that editing was stopped. 265 */ 266 private void fireEditingCanceled() { 267 Object[] listeners = listenerList.getListenerList(); 268 for (int i = listeners.length-2; i>=0; i-=2) { 269 if (listeners[i] == CellEditorListener.class) 270 ((CellEditorListener)listeners[i+1]).editingCanceled(changeEvent); 271 } 272 } 273 274 /** 275 * An action for stopping editing of a cell. 276 * @author Einar Pehrson 277 */ 278 protected class StopAction extends AbstractAction { 279 280 /** The number of columns to move the selection down */ 281 private int columns = 0; 282 283 /** The number of rows to move the selection to the right */ 284 private int rows = 0; 285 286 /** 287 * Creates an edit stopping action. When the action is invoked 288 * the active cell selection is moved the given number of columns 289 * and rows. 290 * @param columns the number of columns to move the selection down 291 * @param rows the number of rows to move the selection to the right 292 */ 293 public StopAction(int columns, int rows) { 294 // Stores members 295 this.columns = columns; 296 this.rows = rows; 297 } 298 299 public void actionPerformed(ActionEvent event) { 300 if (stopCellEditing() && cell != null) { 301 // Transfers focus away from the text field 302 transferFocus(); 303 304 // Moves the active cell selection one row down 305 int column = cell.getAddress().getColumn() + columns; 306 int row = cell.getAddress().getRow() + rows; 307 if (column >= 0 && row >= 0) { 308 Address address = new Address(column, row); 309 Cell cell = uiController.getActiveSpreadsheet().getCell(address); 310 uiController.setActiveCell(cell); 311 } 312 } 313 } 314 } 315 316 /** 317 * An action for cancelling editing of a cell. 318 * @author Einar Pehrson 319 */ 320 @SuppressWarnings("serial") 321 protected class CancelAction extends AbstractAction { 322 323 /** 324 * Creates an edit cancelling action. 325 */ 326 public CancelAction() { 327 // Configures action 328 putValue(NAME, CANCEL_COMMAND); 329 putValue(SHORT_DESCRIPTION, CANCEL_COMMAND); 330 putValue(ACTION_COMMAND_KEY, CANCEL_COMMAND); 331 } 332 333 public void actionPerformed(ActionEvent event) { 334 cancelCellEditing(); 335 } 336 } 337}