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.Component;
024 import java.awt.event.ActionEvent;
025 import java.awt.event.KeyEvent;
026 import java.awt.event.MouseEvent;
027 import java.util.EventObject;
028
029 import javax.swing.AbstractAction;
030 import javax.swing.JOptionPane;
031 import javax.swing.JTable;
032 import javax.swing.JTextField;
033 import javax.swing.KeyStroke;
034 import javax.swing.SwingUtilities;
035 import javax.swing.event.CellEditorListener;
036 import javax.swing.event.ChangeEvent;
037 import javax.swing.table.TableCellEditor;
038 import javax.swing.text.Document;
039 import javax.swing.text.PlainDocument;
040
041 import csheets.core.Address;
042 import csheets.core.Cell;
043 import csheets.core.formula.compiler.FormulaCompilationException;
044 import csheets.core.formula.lang.UnknownElementException;
045 import csheets.ui.ctrl.SelectionEvent;
046 import csheets.ui.ctrl.SelectionListener;
047 import 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")
054 public 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 }