package org.geogebra.web.web.gui.view.spreadsheet;
import org.geogebra.common.awt.GPoint;
import org.geogebra.common.kernel.Kernel;
import org.geogebra.common.kernel.geos.GeoElement;
import org.geogebra.common.main.GWTKeycodes;
import org.geogebra.common.plugin.EventType;
import org.geogebra.common.plugin.GeoClass;
import org.geogebra.web.html5.gui.inputfield.AutoCompleteTextFieldW;
import org.geogebra.web.html5.main.AppW;
import org.geogebra.web.html5.util.SpreadsheetTableModelW;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
public class SpreadsheetKeyListenerW implements KeyDownHandler, KeyPressHandler {
private AppW app;
private SpreadsheetViewW view;
private Kernel kernel;
private MyTableW table;
private SpreadsheetTableModelW model;
private MyCellEditorW editor;
boolean keyDownSomething = false;
public SpreadsheetKeyListenerW(AppW app, MyTableW table) {
this.app = app;
this.kernel = app.getKernel();
this.table = table;
this.view = (SpreadsheetViewW) table.getView();
this.model = (SpreadsheetTableModelW) table.getModel();
this.editor = table.getEditor();
}
@Override
@SuppressFBWarnings({ "SF_SWITCH_FALLTHROUGH",
"missing break is deliberate" })
public void onKeyDown(KeyDownEvent e) {
e.stopPropagation();
// cancel as this may prevent the keyPress in some browsers
// hopefully it is enough to preventDefault in onKeyPress
// e.preventDefault();
// pass the event on to the cell editor if editing
if (table.isEditing) {
table.sendEditorKeyDownEvent(e);
return;
}
table.allowAutoEdit();
int keyCode = e.getNativeKeyCode();// .getKeyCode();
// Application.debug(keyCode+"");
// boolean shiftDown = e.isShiftDown();
boolean ctrlDown = e.isControlKeyDown() || e.isMetaKeyDown();
// AppW.isControlDown(e) // Windows ctrl/Mac Meta
// || e.isControlDown(); // Fudge (Mac ctrl key)
int row = table.getSelectedRow();
int column = table.getSelectedColumn();
switch (keyCode) {
case KeyCodes.KEY_UP:// KeyEvent.VK_UP:
e.preventDefault();
if (e.isControlKeyDown()) {
// AppW.isControlDown(e)) {
if (model.getValueAt(row, column) != null) {
// move to top of current "block"
// if shift pressed, select cells too
while (row > 0 && model.getValueAt(row - 1, column) != null) {
row--;
}
table.changeSelection(row, column,
e.isShiftKeyDown());
} else {
// move up to next defined cell
while (row > 0 && model.getValueAt(row - 1, column) == null) {
row--;
}
table.changeSelection(Math.max(0, row - 1), column, false);
}
// e.consume();
} else {
// default action
row = table.getLeadSelectionRow();
column = table.getLeadSelectionColumn();
if (row > 0) {
table.changeSelection(row - 1, column, e.isShiftKeyDown());
}
}
// copy description into input bar when a cell is entered
// GeoElement geo = (GeoElement)
// getModel().getValueAt(table.getSelectedRow() - 1,
// table.getSelectedColumn());
// if (geo != null) {
// AlgebraInput ai =
// (AlgebraInput)(app.getGuiManager().getAlgebraInput());
// ai.setString(geo);
// }
break;
case KeyCodes.KEY_LEFT:// VK_LEFT:
e.preventDefault();
if (e.isControlKeyDown()) {
// AppD.isControlDown(e)) {
if (model.getValueAt(row, column) != null) {
// move to left of current "block"
// if shift pressed, select cells too
while (column > 0
&& model.getValueAt(row, column - 1) != null) {
column--;
}
table.changeSelection(row, column, e.isShiftKeyDown());
} else {
// move left to next defined cell
while (column > 0
&& model.getValueAt(row, column - 1) == null) {
column--;
}
table.changeSelection(row, Math.max(0, column - 1), false);
}
// e.consume();
} else {
// default action
row = table.getLeadSelectionRow();
column = table.getLeadSelectionColumn();
if (column > 0) {
table.changeSelection(row, column - 1, e.isShiftKeyDown());
}
}
// // copy description into input bar when a cell is entered
// geo = (GeoElement) getModel().getValueAt(table.getSelectedRow(),
// table.getSelectedColumn() - 1);
// if (geo != null) {
// AlgebraInput ai =
// (AlgebraInput)(app.getGuiManager().getAlgebraInput());
// ai.setString(geo);
// }
break;
case KeyCodes.KEY_DOWN:// VK_DOWN:
e.preventDefault();
// auto increase spreadsheet size when you go off the bottom
if (table.getSelectedRow() + 1 >= table.getRowCount()
&& table.getSelectedRow() + 1 < app
.getMaxSpreadsheetRowsVisible()) {
model.setRowCount(table.getRowCount() + 1);
// getView().getRowHeader().revalidate(); //G.STURR 2010-1-9
table.changeSelection(row + 1, column, e.isShiftKeyDown());
} else if (e.isControlKeyDown()) {
// AppD.isControlDown(e)) {
if (model.getValueAt(row, column) != null) {
// move to bottom of current "block"
// if shift pressed, select cells too
while (row < table.getRowCount() - 1
&& model.getValueAt(row + 1, column) != null) {
row++;
}
table.changeSelection(row, column, e.isShiftKeyDown());
} else {
// move down to next selected cell
while (row < table.getRowCount() - 1
&& model.getValueAt(row + 1, column) == null) {
row++;
}
table.changeSelection(
Math.min(table.getRowCount() - 1, row + 1), column,
e.isShiftKeyDown());
}
// e.consume();
} else {
// default action
row = table.getLeadSelectionRow();
column = table.getLeadSelectionColumn();
if (row < table.getRowCount() - 1) {
table.changeSelection(row + 1, column, e.isShiftKeyDown());
}
}
// // copy description into input bar when a cell is entered
// geo = (GeoElement)
// getModel().getValueAt(table.getSelectedRow()+1,
// table.getSelectedColumn());
// if (geo != null) {
// AlgebraInput ai =
// (AlgebraInput)(app.getGuiManager().getAlgebraInput());
// ai.setString(geo);
// }
break;
case KeyCodes.KEY_HOME:// .VK_HOME:
e.preventDefault();
// if shift pressed, select cells too
if (e.isControlKeyDown()) {
// AppD.isControlDown(e)) {
// move to top left of spreadsheet
table.changeSelection(0, 0, e.isShiftKeyDown());
} else {
// move to left of current row
table.changeSelection(row, 0, e.isShiftKeyDown());
}
// e.consume();
break;
case KeyCodes.KEY_END:// .VK_END:
e.preventDefault();
// move to bottom right of spreadsheet
// if shift pressed, select cells too
// find rectangle that will contain all cells
for (int c = 0; c < table.getColumnCount() - 1; c++) {
for (int r = 0; r < table.getRowCount() - 1; r++) {
if ((r > row || c > column)
&& model.getValueAt(r, c) != null) {
if (r > row) {
row = r;
}
if (c > column) {
column = c;
}
}
}
}
table.changeSelection(row, column, e.isShiftKeyDown());
// e.consume();
break;
case KeyCodes.KEY_RIGHT: // Event.VK_RIGHT:
e.preventDefault();
// auto increase spreadsheet size when you go off the right
if (table.getSelectedColumn() + 1 >= table.getColumnCount()
&& table.getSelectedColumn() + 1 < app
.getMaxSpreadsheetColumnsVisible()) {
// table.setRepaintAll();
model.setColumnCount(table.getColumnCount() + 1);
// view.columnHeaderRevalidate();
// view.repaint();//FIXME: setRepaintAll is not compatible with
// TimerSystemW!
// table.repaint();
// view.getFocusPanel().setWidth(table.getGrid().getOffsetWidth()+"px");
// these two lines are a workaround for Java 6
// (Java bug?)
table.changeSelection(row, column + 1, false);
} else if (e.isControlKeyDown()) {
// AppD.isControlDown(e)) {
if (model.getValueAt(row, column) != null) {
// move to bottom of current "block"
// if shift pressed, select cells too
while (column < table.getColumnCount() - 1
&& model.getValueAt(row, column + 1) != null) {
column++;
}
table.changeSelection(row, column, e.isShiftKeyDown());
} else {
// move right to next defined cell
while (column < table.getColumnCount() - 1
&& model.getValueAt(row, column + 1) == null) {
column++;
}
table.changeSelection(row,
Math.min(table.getColumnCount() - 1, column + 1),
false);
}
// e.consume();
} else {
// default action
row = table.getLeadSelectionRow();
column = table.getLeadSelectionColumn();
if (column < table.getColumnCount() - 1) {
table.changeSelection(row, column + 1, e.isShiftKeyDown());
}
}
// // copy description into input bar when a cell is entered
// geo = (GeoElement) getModel().getValueAt(table.getSelectedRow(),
// table.getSelectedColumn() + 1);
// if (geo != null) {
// AlgebraInput ai =
// (AlgebraInput)(app.getGuiManager().getAlgebraInput());
// ai.setString(geo);
// }
break;
case KeyCodes.KEY_SHIFT:// .VK_SHIFT:
case KeyCodes.KEY_CTRL:// Event.VK_CONTROL:
case KeyCodes.KEY_ALT:// Event.VK_ALT:
// case KeyEvent.VK_META: //MAC_OS Meta
// e.consume(); // stops editing start
break;
case GWTKeycodes.KEY_F9:// Event.VK_F9:
kernel.updateConstruction();
// e.consume(); // stops editing start
break;
case GWTKeycodes.KEY_R:// KeyEvent.VK_R:
if (e.isControlKeyDown()) {
// AppD.isControlDown(e)) {
kernel.updateConstruction();
// e.consume();
} else {
letterOrDigitTyped();
}
break;
// needs to be here to stop keypress starting a cell edit after the undo
case GWTKeycodes.KEY_Z:// KeyEvent.VK_Z: //undo
if (ctrlDown) {
// Application.debug("undo");
app.getGuiManager().undo();
// e.consume();
} else {
letterOrDigitTyped();
}
break;
// needs to be here to stop keypress starting a cell edit after the redo
case GWTKeycodes.KEY_Y:// KeyEvent.VK_Y: //redo
if (ctrlDown) {
// Application.debug("redo");
app.getGuiManager().redo();
// e.consume();
} else {
letterOrDigitTyped();
}
break;
case GWTKeycodes.KEY_C:// KeyEvent.VK_C:
case GWTKeycodes.KEY_V:// KeyEvent.VK_V:
case GWTKeycodes.KEY_X:// KeyEvent.VK_X:
if (!editor.isEditing()) {
if (!(ctrlDown || e.isAltKeyDown())) {
letterOrDigitTyped();
}
}
break;
case GWTKeycodes.KEY_DELETE:// KeyEvent.VK_DELETE:
case GWTKeycodes.KEY_BACKSPACE:// KeyEvent.VK_BACK_SPACE:
if (!editor.isEditing()) {
e.preventDefault();
// e.consume();
// Application.debug("deleting...");
boolean storeUndo = table.delete();
if (storeUndo) {
app.storeUndoInfo();
}
return;
}
break;
// case KeyEvent.VK_ENTER:
case GWTKeycodes.KEY_F2:// KeyEvent.VK_F2: //FIXME
if (!editor.isEditing()) {
table.setAllowEditing(true);
table.editCellAt(table.getSelectedRow(),
table.getSelectedColumn());
table.setAllowEditing(false);
}
// e.consume();
break;
case KeyCodes.KEY_ENTER:// KeyEvent.VK_ENTER:
if (!editor.isEditing()) {
editOnEnter();
break;
}
editor.onEnter();
// fall through
case GWTKeycodes.KEY_PAGEDOWN:// KeyEvent.VK_PAGE_DOWN:
e.preventDefault();
int pixelx = table.getPixel(column, row, true).getX();
int pixely = view.getFocusPanel().getAbsoluteTop()
+ view.getFocusPanel().getOffsetHeight();
GPoint gip = table.getIndexFromPixel(pixelx, pixely);
if (gip != null) {
table.changeSelection(gip.getY(), column, false);
} else {
table.changeSelection(model.getRowCount() - 1, column, false);
}
break;
case GWTKeycodes.KEY_PAGEUP:// KeyEvent.VK_PAGE_UP:
e.preventDefault();
int pixx = table.getPixel(column, row, true).getX();
int pixy = view.getFocusPanel().getAbsoluteTop();
GPoint gi = table.getIndexFromPixel(pixx, pixy);
if (gi != null) {
table.changeSelection(gi.getY(), column, false);
// stop cell being erased before moving
} else {
table.changeSelection(0, column, false);
}
break;
// stop TAB erasing cell before moving
case KeyCodes.KEY_TAB:// KeyEvent.VK_TAB:
e.preventDefault();
// disable shift-tab in column A
if (table.getSelectedColumn() == 0 && e.isShiftKeyDown()) {
// e.consume();
} else {
if (e.isShiftKeyDown()) {
// if (table.getSelectedColumn() == 0)
// this cannot happen
table.changeSelection(row, column - 1, false);
} else {
if (table.getSelectedColumn() + 1 >= table.getColumnCount() - 1) {
if (table.getSelectedRow() + 1 < table.getRowCount() - 1) {
table.changeSelection(row + 1, 0, false);
}
} else {
table.changeSelection(row, column + 1, false);
}
}
}
break;
case GWTKeycodes.KEY_A:// KeyEvent.VK_A:
if (e.isControlKeyDown()) {
// AppD.isControlDown(e)) {
// select all cells
row = 0;
column = 0;
// find rectangle that will contain all defined cells
for (int c = 0; c < table.getColumnCount() - 1; c++) {
for (int r = 0; r < table.getRowCount() - 1; r++) {
if ((r > row || c > column)
&& model.getValueAt(r, c) != null) {
if (r > row) {
row = r;
}
if (c > column) {
column = c;
}
}
}
}
table.changeSelection(0, 0, false);
table.changeSelection(row, column, true);
// e.consume();
}
// no break, fall through
default:
if (/*
* ? !Character.isIdentifierIgnorable(
* Character.toChars(e.getNativeEvent().getCharCode())[0]
* //e.getKeyChar() ) &&
*/
!editor.isEditing() && !(ctrlDown || e.isAltKeyDown())) {
letterOrDigitTyped();
} else {
// e.consume();
break;
}
}
}
public void letterOrDigitTyped() {
// memorize that this is OK according to keyCode
keyDownSomething = true;
table.setAllowEditing(true);
table.repaint(); // G.Sturr 2009-10-10: cleanup when keypress edit
// begins
// check if cell fixed
Object o = model.getValueAt(table.getSelectedRow(),
table.getSelectedColumn());
if (o instanceof GeoElement) {
GeoElement geo = (GeoElement) o;
if (geo.isProtected(EventType.UPDATE)) {
return;
}
}
model.setValueAt(null, table.getSelectedRow(),
table.getSelectedColumn());
table.editCellAt(table.getSelectedRow(), table.getSelectedColumn());
table.setAllowEditing(false);
}
private void editOnEnter() {
int row = table.getSelectedRow();
int col = table.getSelectedColumn();
// memorize that this is OK according to keyCode
keyDownSomething = true;
table.setAllowEditing(true);
table.repaint(); // G.Sturr 2009-10-10: cleanup when keypress edit
// begins
// check if cell fixed
Object o = model.getValueAt(table.getSelectedRow(),
table.getSelectedColumn());
if (o instanceof GeoElement) {
GeoElement geo = (GeoElement) o;
if (geo.isProtected(EventType.UPDATE)) {
return;
}
}
model.setValueAt(model.getValueAt(row, col), row, col);
table.editCellAt(row, col);
editor.selectAll();
table.setAllowEditing(false);
}
@Override
public void onKeyPress(KeyPressEvent e) {
// make sure e.g. SHIFT+ doesn't trigger default browser action
e.stopPropagation();
// prevent default action in all cases here except CTRL+V
// but how to detect CTRL+V? Just detect "V" and "v", and
// check e.ctrlKeyDown! This is only needed in Firefox, to
// properly trigger the "paste" event... in other browsers
// we could call preventDefault unconditionally (paste OK)
if (!e.isControlKeyDown()) {
e.preventDefault();
} else if (e.getCharCode() != 86 && e.getCharCode() != 118 && // "V"
e.getCharCode() != 67 && e.getCharCode() != 99 && // "C"
e.getCharCode() != 88 && e.getCharCode() != 120) {// "X"
e.preventDefault();
}
// pass the event on to the cell editor if editing
if (table.isEditing) {
table.sendEditorKeyPressEvent(e);
return;
}
// check if this is OK according to keyCode too (right key)
if (keyDownSomething) {
keyDownSomething = false;
} else {
return;
}
// as the following doesn't work for KeyDownEvent:
// e.getNativeEvent().getCharCode();
final int charcode = e.getUnicodeCharCode();
// make sure that this code runs after the editor has actually been
// created
Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
@Override
public void execute() {
// if the user enters something meaningful, spare an additional
// entering
String str = "";
if (charcode > 0) {
str = new String(Character.toChars(charcode));
}
Object ce = table.getCellEditor();
GeoClass ceType = table.getCellEditorType(
table.getSelectedRow(), table.getSelectedColumn());
if (ce instanceof MyCellEditorW && ceType == GeoClass.DEFAULT
&& !"".equals(str)) {
((MyCellEditorW) ce).setText(str);
((AutoCompleteTextFieldW) ((MyCellEditorW) ce)
.getTextfield())
.setCaretPosition(((MyCellEditorW) ce)
.getEditingValue().length());
}
}
});
}
public native void addPasteHandlerTo(Element elem) /*-{
var self = this;
elem.onpaste = function(event) {
var text, cbd;
if ($wnd.clipboardData) {
// Windows Internet Explorer
cbd = $wnd.clipboardData;
if (cbd.getData) {
text = cbd.getData('Text');
}
}
if (text === undefined) {
// all the other browsers
if (event.clipboardData) {
cbd = event.clipboardData;
if (cbd.getData) {
text = cbd.getData('text/plain');
}
}
}
if (text !== undefined) {
self.@org.geogebra.web.web.gui.view.spreadsheet.SpreadsheetKeyListenerW::onPaste(Ljava/lang/String;)(text);
}
}
elem.oncopy = function(even2) {
self.@org.geogebra.web.web.gui.view.spreadsheet.SpreadsheetKeyListenerW::onCopy(Z)(even2.altKey);
// do not prevent default!!!
// it will take care of the copy...
}
elem.oncut = function(even3) {
self.@org.geogebra.web.web.gui.view.spreadsheet.SpreadsheetKeyListenerW::onCut()();
// do not prevent default!!!
// it will take care of the cut...
}
}-*/;
public void onPaste(String text) {
boolean storeUndo = table.paste(text);
view.rowHeaderRevalidate();
if (storeUndo) {
app.storeUndoInfo();
}
}
public void onCopy(final boolean altDown) {
// the default action of the browser just modifies
// the textarea of the AdvancedFocusPanel, does
// no harm to the other parts of the code, and
// consequently, it should ideally be done before this!
// so let's run the original code afterwards...
// not sure one ScheduleDeferred is enough...
// but in theory, it should be as code continues from
// here towards the default action, as we are in the event
Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
@Override
public void execute() {
table.copy(altDown, true);
}
});
}
public void onCut() {
// the default action of the browser just modifies
// the textarea of the AdvancedFocusPanel, does
// no harm to the other parts of the code, and
// consequently, it should ideally be done before this!
// not sure one ScheduleDeferred is enough...
// but in theory, it should be as code continues from
// here towards the default action, as we are in the event
Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
@Override
public void execute() {
boolean storeUndo = table.cut(true);
if (storeUndo) {
app.storeUndoInfo();
}
}
});
}
}