package org.geogebra.desktop.gui.view.spreadsheet; import java.awt.Component; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import javax.swing.DefaultCellEditor; import javax.swing.JTable; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import org.geogebra.common.gui.view.spreadsheet.RelativeCopy; import org.geogebra.common.gui.view.spreadsheet.SpreadsheetController; import org.geogebra.common.kernel.Kernel; import org.geogebra.common.kernel.geos.GeoElement; import org.geogebra.common.kernel.geos.GeoElementSpreadsheet; import org.geogebra.common.kernel.kernelND.GeoElementND; import org.geogebra.common.plugin.EventType; import org.geogebra.common.util.debug.Log; import org.geogebra.desktop.gui.inputfield.AutoCompleteTextFieldD; import org.geogebra.desktop.gui.inputfield.KeyNavigation; import org.geogebra.desktop.gui.virtualkeyboard.VirtualKeyboardD; import org.geogebra.desktop.main.AppD; /** * Default cell editor for the spreadsheet, extends * DefaultCellEditor(JTextField) * */ public class MyCellEditorSpreadsheet extends DefaultCellEditor implements FocusListener { private static final long serialVersionUID = 1L; protected Kernel kernel; protected AppD app; protected GeoElementND value; protected MyTableD table; AutoCompleteTextFieldD textField; protected int column; protected int row; boolean editing = false; private boolean errorOnStopEditing = false; private boolean allowProcessGeo = false; public boolean allowProcessGeo() { return allowProcessGeo; } public void setAllowProcessGeo(boolean allowProcessGeo) { this.allowProcessGeo = allowProcessGeo; } private boolean enableAutoComplete = false; private SpreadsheetController controller; public boolean isEnableAutoComplete() { return enableAutoComplete; } public void setEnableAutoComplete(boolean enableAutoComplete) { this.enableAutoComplete = enableAutoComplete; textField.setAutoComplete(enableAutoComplete); } public MyCellEditorSpreadsheet(Kernel kernel, SpreadsheetController controller) { super(new AutoCompleteTextFieldD(0, (AppD) kernel.getApplication(), KeyNavigation.IGNORE)); this.kernel = kernel; this.controller = controller; app = (AppD) kernel.getApplication(); textField = (AutoCompleteTextFieldD) editorComponent; textField.setAutoComplete(enableAutoComplete); editorComponent .addKeyListener(new SpreadsheetCellEditorKeyListener(false)); editorComponent.addFocusListener(this); DocumentListener documentListener = new DocumentListener() { @Override public void changedUpdate(DocumentEvent documentEvent) { // do nothing } @Override public void insertUpdate(DocumentEvent documentEvent) { updateFormulaBar(documentEvent); } @Override public void removeUpdate(DocumentEvent documentEvent) { updateFormulaBar(documentEvent); } private void updateFormulaBar(DocumentEvent documentEvent) { if (table.view.getShowFormulaBar() && (textField.hasFocus() || table.isDragging2)) { table.view.getFormulaBar() .setEditorText(textField.getText()); } } }; textField.getDocument().addDocumentListener(documentListener); } public void setText(String text) { if (!textField.hasFocus() && !table.isDragging2) { textField.setText(text); } } @Override public Component getTableCellEditorComponent(JTable table0, Object value0, boolean isSelected, int row0, int column0) { if (table0 instanceof MyTableD) { table = (MyTableD) table0; } else { return null; } if (value0 instanceof String) { // clicked to type value = null; } else { value = (GeoElement) value0; } column = column0; row = row0; String text = ""; if (value != null) { text = controller.getEditorInitString(value); int index = text.indexOf("="); if ((!value.isGeoText())) { if (index == -1) { text = "=" + text; } } } delegate.setValue(text); Component component = getComponent(); component.setFont(app.getFontCanDisplayAwt(text)); editing = true; return component; } /** * set flag to require text start with "=" to activate autocomplete */ public void setEqualsRequired(boolean equalsRequired) { textField.setEqualsRequired(equalsRequired); } /** * returns flag that requires text start with "=" to activate autocomplete */ public boolean isEqualsRequired() { return textField.isEqualsRequired(); } public void setLabels() { textField.setDictionary(true); } /** * * @return true if the completion popup is open */ public boolean completionsPopupOpen() { return textField.getCompletions() != null; } // ======================================================= // In-cell Editing Methods // ======================================================= public boolean isEditing() { return editing; } public int getCaretPosition() { return textField.getCaretPosition(); } /** Insert a geo label into current editor string. */ public void addLabel(String label) { if (!editing) { return; } // String text = (String) delegate.getCellEditorValue(); // delegate.setValue(text + label); textField.replaceSelection(" " + label + " "); } public void setLabel(String text) { if (!editing) { return; } delegate.setValue(text); } public String getEditingValue() { return (String) delegate.getCellEditorValue(); } @Override public Object getCellEditorValue() { return value; } // ======================================================= // Stop/Cancel Editing // ======================================================= @Override public void cancelCellEditing() { editing = false; errorOnStopEditing = false; super.cancelCellEditing(); // give the table the focus in case the formula bar is the editor if (table.getView().getFormulaBar().editorHasFocus()) { // Application.debug("give focus to table"); table.requestFocus(); } } @Override public boolean stopCellEditing() { errorOnStopEditing = true; // flag to handle column resizing during // editing (see focusLost method) // try to redefine or create the cell geo with the current editing // string if (!processGeo()) { return false; } errorOnStopEditing = false; editing = false; boolean success = super.stopCellEditing(); // give the table the focus in case the formula bar is the editor if (table.getView().getFormulaBar().editorHasFocus()) { // Application.debug("give focus to table"); table.requestFocus(); } return success; } boolean stopCellEditing(int colOff, int rowOff) { allowProcessGeo = true; boolean success = stopCellEditing(); moveSelectedCell(colOff, rowOff); allowProcessGeo = false; return success; } private void moveSelectedCell(int colOff, int rowOff) { int nextRow = Math.min(row + rowOff, table.getRowCount() - 1); int nextColumn = Math.min(column + colOff, table.getColumnCount() - 1); table.setSelection(nextColumn, nextRow); } /** * Attempts to create or redefine the cell geo using the current editing * string * * @return */ private boolean processGeo() { try { if (allowProcessGeo) { String text = (String) delegate.getCellEditorValue(); // get GeoElement of current cell value = kernel.lookupLabel(GeoElementSpreadsheet .getSpreadsheetCellName(column, row)); if ("".equals(text)) { if (value != null) { value.removeOrSetUndefinedIfHasFixedDescendent(); value = null; } } else { GeoElementND newVal = RelativeCopy .prepareAddingValueToTableNoStoringUndoInfo(kernel, app, text, value, column, row, false); if (newVal == null) { return false; } value = newVal; } if (value != null) { app.storeUndoInfo(); } } } catch (Exception ex) { // show GeoGebra error dialog // kernel.getApplication().showError(ex.getMessage()); ex.printStackTrace(); super.stopCellEditing(); editing = false; return false; } return true; } // ======================================================= // Key and Focus Listeners // ======================================================= /** * keep track of when <tab> was first pressed so we can return to that * column when <enter> pressed */ public int tabReturnCol = -1; public class SpreadsheetCellEditorKeyListener implements KeyListener { // boolean escape = false; boolean isFormulaBarListener; public SpreadsheetCellEditorKeyListener(boolean isFormulaBarListener) { this.isFormulaBarListener = isFormulaBarListener; } @Override public void keyTyped(KeyEvent e) { // } @Override public void keyPressed(KeyEvent e) { checkCursorKeys(e); int keyCode = e.getKeyCode(); if (keyCode == KeyEvent.VK_ESCAPE) { GeoElement oldGeo = kernel.getGeoAt(column, row); cancelCellEditing(); // restore old text in spreadsheet table.getModel().setValueAt(oldGeo, row, column); // stopCellEditing(0,0); // force nice redraw table.setSelection(column, row); // update the formula bar after escape table.getView().updateFormulaBar(); } } @Override public void keyReleased(KeyEvent e) { // } public void checkCursorKeys(KeyEvent e) { String text = (String) delegate.getCellEditorValue(); int keyCode = e.getKeyCode(); // Application.debug(e+""); switch (keyCode) { default: // do nothing break; case KeyEvent.VK_UP: if (isFormulaBarListener) { return; } // Application.debug("UP"); stopCellEditing(0, -1); editing = false; e.consume(); tabReturnCol = -1; break; case KeyEvent.VK_TAB: if (isFormulaBarListener) { return; } Log.debug(" tab"); // Application.debug("RIGHT"); // shift-tab moves left // tab moves right if (tabReturnCol == -1) { tabReturnCol = column; } stopCellEditing(e.isShiftDown() ? -1 : 1, 0); editing = false; break; case KeyEvent.VK_ENTER: // if incomplete command entered, want to move the cursor to // between [] int bracketsIndex = text.indexOf("[]"); if (bracketsIndex == -1) { if (tabReturnCol != -1) { int colOffset = tabReturnCol - column; stopCellEditing(colOffset, 1); editing = false; } else { String cellBelowStr = GeoElementSpreadsheet .getSpreadsheetCellName(column, row + 1); GeoElement cellBelow = kernel.getConstruction() .lookupLabel(cellBelowStr); boolean moveDown = cellBelow == null || !cellBelow.isProtected(EventType.UPDATE); // don't move down to cell below after <Enter> if it's // fixed stopCellEditing(0, moveDown ? 1 : 0); } } else { textField.setCaretPosition(bracketsIndex + 1); e.consume(); } tabReturnCol = -1; break; case KeyEvent.VK_DOWN: if (isFormulaBarListener) { e.consume(); return; } // Application.debug("DOWN"); stopCellEditing(0, 1); editing = false; tabReturnCol = -1; break; case KeyEvent.VK_LEFT: if (isFormulaBarListener) { return; } // Application.debug("LEFT"); // Allow left/right keys to exit cell for easier data entry if (getCaretPosition() == 0) { stopCellEditing(-1, 0); editing = false; } editing = false; tabReturnCol = -1; break; case KeyEvent.VK_RIGHT: if (isFormulaBarListener) { return; } // Application.debug("RIGHT"); // Allow left/right keys to exit cell for easier data entry if (getCaretPosition() == text.length()) { stopCellEditing(1, 0); editing = false; } editing = false; tabReturnCol = -1; break; case KeyEvent.VK_PAGE_DOWN: case KeyEvent.VK_PAGE_UP: e.consume(); tabReturnCol = -1; break; // An F1 keypress causes the focus to be lost, so we // need to set 'editing' to false to prevent the focusLost() // method from calling stopCellEditing() case KeyEvent.VK_F1: editing = false; break; } } } @Override public void focusGained(FocusEvent arg0) { editing = true; } @Override public void focusLost(FocusEvent arg0) { // VirtualKeyboard gets the focus very briefly when opened // so ignore this! if (arg0.getOppositeComponent() instanceof VirtualKeyboardD) { return; } // only needed if eg columns resized if (editing) { if (!errorOnStopEditing) { // Process the current edit, exit the editor and update the // formula bar setAllowProcessGeo(true); stopCellEditing(); setAllowProcessGeo(false); table.getView().getFormulaBar().update(); } else if (!app.isErrorDialogShowing()) { cancelCellEditing(); } } } }