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}