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    }