package org.geogebra.web.web.gui.view.spreadsheet; import org.geogebra.common.awt.GPoint; import org.geogebra.common.awt.GRectangle; import org.geogebra.common.gui.view.spreadsheet.MyTable; import org.geogebra.common.gui.view.spreadsheet.MyTableInterface; import org.geogebra.common.gui.view.spreadsheet.RelativeCopy; import org.geogebra.common.kernel.geos.GeoElement; import org.geogebra.common.kernel.geos.GeoElementSpreadsheet; import org.geogebra.common.main.App; import org.geogebra.common.main.SpreadsheetTableModel; import org.geogebra.common.plugin.GeoClass; import org.geogebra.ggbjdk.java.awt.geom.Rectangle2D; import org.geogebra.web.html5.gui.util.CancelEventTimer; import org.geogebra.web.html5.gui.util.LongTouchManager; import org.geogebra.web.html5.gui.util.LongTouchTimer.LongTouchHandler; import org.geogebra.web.html5.main.AppW; import org.geogebra.web.html5.util.EventUtil; import org.geogebra.web.web.gui.GuiManagerW; import org.geogebra.web.web.javax.swing.GPopupMenuW; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.event.dom.client.DomEvent; import com.google.gwt.event.dom.client.DoubleClickEvent; import com.google.gwt.event.dom.client.DoubleClickHandler; import com.google.gwt.event.dom.client.MouseDownEvent; import com.google.gwt.event.dom.client.MouseDownHandler; import com.google.gwt.event.dom.client.MouseMoveEvent; import com.google.gwt.event.dom.client.MouseMoveHandler; import com.google.gwt.event.dom.client.MouseUpEvent; import com.google.gwt.event.dom.client.MouseUpHandler; import com.google.gwt.event.dom.client.TouchEndEvent; import com.google.gwt.event.dom.client.TouchEndHandler; import com.google.gwt.event.dom.client.TouchMoveEvent; import com.google.gwt.event.dom.client.TouchMoveHandler; import com.google.gwt.event.dom.client.TouchStartEvent; import com.google.gwt.event.dom.client.TouchStartHandler; import com.google.gwt.regexp.shared.MatchResult; import com.google.gwt.user.client.Window; public class SpreadsheetMouseListenerW implements MouseDownHandler, MouseUpHandler, MouseMoveHandler, DoubleClickHandler, TouchStartHandler, TouchEndHandler, TouchMoveHandler, LongTouchHandler { protected String selectedCellName; protected String prefix, postfix; private final AppW app; private SpreadsheetViewW view; private MyTableW table; private SpreadsheetTableModel model; private MyCellEditorW editor; private RelativeCopy relativeCopy; private boolean pointerIsDown = false; private final static boolean editEnabled = true; private LongTouchManager longTouchManager; private int numberOfTouches = 0; /************************************************* * Constructor */ public SpreadsheetMouseListenerW(AppW app, MyTableW table) { this.app = app; this.table = table; view = (SpreadsheetViewW) table.getView(); model = table.getModel(); editor = table.getEditor(); relativeCopy = new RelativeCopy(app.getKernel()); longTouchManager = LongTouchManager.getInstance(); } @Override public void handleLongTouch(int x, int y) { showContextMenu(x, y); } public static int getAbsoluteX(DomEvent e, AppW app) { return (int) ((EventUtil.getTouchOrClickClientX(e) + Window .getScrollLeft()) / app.getArticleElement().getScaleX()); } public int getAbsoluteX(DomEvent e) { return getAbsoluteX(e, app); } public static int getAbsoluteY(DomEvent e, AppW app) { return (int) ((EventUtil.getTouchOrClickClientY(e) + Window .getScrollTop()) / app.getArticleElement().getScaleY()); } public int getAbsoluteY(DomEvent e) { return getAbsoluteY(e, app); } private GPoint getIndexFromEvent(DomEvent<?> event) { return table .getIndexFromPixel(getAbsoluteX(event), getAbsoluteY(event)); } private GPoint getPixelFromEvent(DomEvent<?> event) { return new GPoint(getAbsoluteX(event), getAbsoluteY(event)); } @Override public void onDoubleClick(DoubleClickEvent doubleClickEvent) { if (!editEnabled) { return; } if (table.isOverDot) { // auto-fill down if dragging dot is double-clicked // TODO handleAutoFillDown(); return; } // otherwise, doubleClick edits cell GPoint point = getIndexFromEvent(doubleClickEvent); tryEditCellAt(point); } private void tryEditCellAt(GPoint point) { if (canEditCellAt(point) && !isEditingCellAt(point)) { editCellAt(point); } } private boolean canEditCellAt(GPoint point) { return !(table.getOneClickEditMap().containsKey(point) && view .allowSpecialEditor()); } private boolean isEditingCellAt(GPoint point) { int column = point.getX(); int row = point.getY(); return editor.isEditing() && editor.row == row && editor.column == column; } private void editCellAt(GPoint point) { table.setAllowEditing(true); table.editCellAt(point); table.setAllowEditing(false); } @Override public void onMouseDown(MouseDownEvent mouseDownEvent) { focusKeyDispatcher(); if (CancelEventTimer.cancelMouseEvent()) { return; } handlePointerDown(mouseDownEvent); } private void focusKeyDispatcher() { if (app != null) { app.getGlobalKeyDispatcher().setFocused(true); } } @Override public void onTouchStart(TouchStartEvent touchStartEvent) { focusKeyDispatcher(); numberOfTouches = touchStartEvent.getTouches().length(); if (numberOfTouches == 1) { updateTableIsOverDot(touchStartEvent); handlePointerDown(touchStartEvent); longTouchManager.scheduleTimer(this, getAbsoluteX(touchStartEvent), getAbsoluteY(touchStartEvent)); } // else there are double (or more) touches // and we are scrolling CancelEventTimer.touchEventOccured(); } private void handlePointerDown(DomEvent<?> event) { setActiveToolbarIfNecessary(); //event.preventDefault(); if (!editEnabled) { return; } GPoint point = getIndexFromEvent(event); if (point == null) { return; } pointerIsDown = true; if (editor.isEditing()) { if (editor.textStartsWithEquals() && copyIntoEditorFromCellAt(point)) { startEditDragging(); event.preventDefault(); return; } // selecting the same cell should not finish editing // e.g. move cursor inside cell if (!isCurrentSelection(point)) { if (table.getEditor() != null) { table.getEditor().stopCellEditing(); } finishEditing(); } } // request focus only if there will be no editing // else the view steals the focus from the input if (!(EventUtil.isTouchEvent(event) && isCurrentSelection(point))) { view.requestFocus(); } if (table.isOverDot) { if (table.showCanDragBlueDot()) { table.isDragingDot = true; } } else { if (!isCurrentSelection(point)) { if (!isInsideCurrentSelection(point) || !isRightClick(event)) { changeSelection(point, false); } } else if (EventUtil.isTouchEvent(event.getNativeEvent())) { tryEditCellAt(point); } // force column selection if (view.isColumnSelect()) { int column = point.getX(); table.setColumnSelectionInterval(column, column); } } } private void changeSelection(GPoint point, boolean extend) { if (table.getSelectionType() != MyTableInterface.CELL_SELECT) { table.setSelectionType(MyTableInterface.CELL_SELECT); } table.changeSelection(point, extend); } private void setActiveToolbarIfNecessary() { if ((app.getGuiManager() != null) && app.showToolBar()) { ((GuiManagerW) app.getGuiManager()) .setActivePanelAndToolbar(App.VIEW_SPREADSHEET); } } private boolean copyIntoEditorFromCellAt(GPoint pointOnMouseDown) { int column = pointOnMouseDown.getX(); int row = pointOnMouseDown.getY(); GeoClass cellType = table.getCellEditorType(row, column); if (column == editor.column && row == editor.row || cellType == GeoClass.BUTTON || cellType == GeoClass.BOOLEAN) { return false; } GeoElement geo = RelativeCopy.getValue(app, column, row); if (geo != null) { // get cell name String name = GeoElementSpreadsheet.getSpreadsheetCellName(column, row); if (geo.isGeoFunction()) { name += "(x)"; // TODO this should not be here // (x) should be coming from the geo itself } selectedCellName = name; // insert the geo label into the editor string editor.addLabel(name); return true; } return false; } private void startEditDragging() { int caretPos = editor.getCaretPosition(); String text = editor.getEditingValue(); prefix = text.substring(0, caretPos); postfix = text.substring(caretPos, text.length()); table.isDragging = true; } private void finishEditing() { editor.setAllowProcessGeo(true); editor.stopCellEditing(); editor.setAllowProcessGeo(false); table.finishEditing(false); } private boolean isCurrentSelection(GPoint point) { return isInsideCurrentSelection(point) && singleCellSelected(); } @Override public void onMouseUp(MouseUpEvent event) { if (CancelEventTimer.cancelMouseEvent()) { return; } handlePointerUp(event); } @Override public void onTouchEnd(TouchEndEvent event) { longTouchManager.cancelTimer(); numberOfTouches = event.getChangedTouches().length(); if (numberOfTouches == 1) { handlePointerUp(event); } CancelEventTimer.touchEventOccured(); } private void handlePointerUp(DomEvent<?> event) { if (!editEnabled) { return; } pointerIsDown = false; event.preventDefault(); GPoint point = getIndexFromEvent(event); if (table.getTableMode() == MyTable.TABLE_MODE_AUTOFUNCTION) { table.getSpreadsheetModeProcessor().stopAutoFunction(); return; } if (isRightClick(event) && app.letShowPopupMenu()) { showContextMenu(event); } if (table.isDragingDot) { boolean success = doDragCopy(); if (success) { app.storeUndoInfo(); } resetDraggingFlags(); } // Alt click: copy definition to input field if (!table.isEditing() && event.getNativeEvent().getAltKey() && app.showAlgebraInput()) { GeoElement geo = RelativeCopy.getValue(app, point); if (geo != null) { // F3 key: copy definition to input bar app.getGlobalKeyDispatcher().handleFunctionKeyForAlgebraInput( 3, geo); } } resetState(); table.repaint(); } private void showContextMenu(DomEvent<?> event) { int x = EventUtil.getTouchOrClickClientX(event); int y = EventUtil.getTouchOrClickClientY(event); showContextMenu(x, y); } private void showContextMenu(int x, int y) { SpreadsheetContextMenuW contextMenu = ((GuiManagerW) app .getGuiManager()).getSpreadsheetContextMenu(table); GPopupMenuW popupMenu = (GPopupMenuW) contextMenu.getMenuContainer(); popupMenu.show(new GPoint(x, y)); app.registerPopup(popupMenu.getPopupPanel()); } private boolean doDragCopy() { if (table.draggingToColumn == -1 || table.draggingToRow == -1) { return false; } int x1 = -1; int y1 = -1; int x2 = -1; int y2 = -1; // -|1|- // 2|-|3 // -|4|- if (table.draggingToColumn < table.minSelectionColumn) { // 2 x1 = table.draggingToColumn; y1 = table.minSelectionRow; x2 = table.minSelectionColumn - 1; y2 = table.maxSelectionRow; } else if (table.draggingToRow > table.maxSelectionRow) { // 4 x1 = table.minSelectionColumn; y1 = table.maxSelectionRow + 1; x2 = table.maxSelectionColumn; y2 = table.draggingToRow; } else if (table.draggingToRow < table.minSelectionRow) { // 1 x1 = table.minSelectionColumn; y1 = table.draggingToRow; x2 = table.maxSelectionColumn; y2 = table.minSelectionRow - 1; } else if (table.draggingToColumn > table.maxSelectionColumn) { // 3 x1 = table.maxSelectionColumn + 1; y1 = table.minSelectionRow; x2 = table.draggingToColumn; y2 = table.maxSelectionRow; } // copy the cells boolean succ = relativeCopy.doDragCopy(table.minSelectionColumn, table.minSelectionRow, table.maxSelectionColumn, table.maxSelectionRow, x1, y1, x2, y2); // extend the selection to include the drag copy selection table.setSelection(Math.min(x1, table.minSelectionColumn), Math.min(y1, table.minSelectionRow), Math.max(x2, table.maxSelectionColumn), Math.max(y2, table.maxSelectionRow)); return succ; } private void resetDraggingFlags() { table.isOverDot = false; table.isDragingDot = false; table.draggingToRow = -1; table.draggingToColumn = -1; } private boolean isInsideCurrentSelection(GPoint point) { int column = point.getX(); int row = point.getY(); return row >= table.minSelectionRow && row <= table.maxSelectionRow && column >= table.minSelectionColumn && column <= table.maxSelectionColumn; } private boolean singleCellSelected() { return table.minSelectionRow == table.maxSelectionRow && table.minSelectionColumn == table.maxSelectionColumn; } private static boolean isRightClick(DomEvent<?> event) { if (EventUtil.isTouchEvent(event)) { return false; // right click is handled by rightClickTimer } return event.getNativeEvent().getButton() == NativeEvent.BUTTON_RIGHT; } private void resetState() { selectedCellName = null; prefix = null; postfix = null; table.isDragging = false; } @Override public void onTouchMove(TouchMoveEvent event) { numberOfTouches = event.getTouches().length(); if (numberOfTouches == 1) { event.stopPropagation(); handlePointerMove(event); longTouchManager.rescheduleTimerIfRunning(this, getAbsoluteX(event), getAbsoluteY(event), false); } else { longTouchManager.cancelTimer(); } CancelEventTimer.touchEventOccured(); } @Override public void onMouseMove(MouseMoveEvent event) { if(CancelEventTimer.cancelMouseEvent()){ return; } handlePointerMove(event); } private void handlePointerMove(DomEvent<?> event) { if (!editEnabled) { return; } GPoint point = getIndexFromEvent(event); event.preventDefault(); boolean eConsumed = false; if (pointerIsDown) { if (table.getTableMode() == MyTable.TABLE_MODE_AUTOFUNCTION || table.getTableMode() == MyTable.TABLE_MODE_DROP) { // App.debug("drop is dragging "); return; } // handle editing mode drag if (editor.isEditing()) { // GPoint point = table.getIndexFromPixel(getAbsoluteX(event), // getAbsoluteY(event)); if (point != null && selectedCellName != null) { int column2 = point.getX(); int row2 = point.getY(); MatchResult matcher = GeoElementSpreadsheet.spreadsheetPattern .exec(selectedCellName); int column1 = GeoElementSpreadsheet .getSpreadsheetColumn(matcher); int row1 = GeoElementSpreadsheet.getSpreadsheetRow(matcher); if (column1 > column2) { int temp = column1; column1 = column2; column2 = temp; } if (row1 > row2) { int temp = row1; row1 = row2; row2 = temp; } String name1 = GeoElementSpreadsheet .getSpreadsheetCellName(column1, row1); String name2 = GeoElementSpreadsheet .getSpreadsheetCellName(column2, row2); if (!name1.equals(name2)) { name1 += ":" + name2; } name1 = prefix + name1 + postfix; editor.setLabel(name1); table.repaint(); } return; } // handle dot drag if (table.isDragingDot) { eConsumed = true; int mouseX = getAbsoluteX(event); int mouseY = getAbsoluteY(event); GPoint mouseCell = table.getIndexFromPixel(mouseX, mouseY); if (mouseCell == null) { // user has dragged outside the table, // to // left or above table.draggingToRow = -1; table.draggingToColumn = -1; } else { table.draggingToRow = mouseCell.getY(); table.draggingToColumn = mouseCell.getX(); GRectangle selRect = table.getSelectionRect(true); // increase size if we're at the bottom of the spreadsheet if (table.draggingToRow + 1 == table.getRowCount() && table.draggingToRow < app .getMaxSpreadsheetRowsVisible()) { model.setRowCount(table.getRowCount() + 1); } // increase size if we go beyond the right edge if (table.draggingToColumn + 1 == table.getColumnCount() && table.draggingToColumn < app .getMaxSpreadsheetColumnsVisible()) { model.setColumnCount(table.getColumnCount() + 1); // view.columnHeaderRevalidate(); // Java's addColumn method will clear selection, so // re-select our cell // table.setSelection(oldSelection); } // scroll to show "highest" selected cell table.scrollRectToVisible(table.getCellRect(mouseCell.y, mouseCell.x, true)); if (!selRect.contains(getAbsoluteX(event), getAbsoluteY(event))) { int rowOffset = 0, colOffset = 0; // get row distance if (table.minSelectionRow > 0 && table.draggingToRow < table.minSelectionRow) { rowOffset = mouseY - (int) selRect.getY(); if (-rowOffset < 0.5 * table.getCellRect( table.minSelectionRow - 1, table.minSelectionColumn, true).getHeight()) { rowOffset = 0; } } else if (table.maxSelectionRow < app .getMaxSpreadsheetRowsVisible() && table.draggingToRow > table.maxSelectionRow) { rowOffset = mouseY - ((int) selRect.getY() + (int) selRect .getHeight()); if (rowOffset < 0.5 * table.getCellRect( table.maxSelectionRow + 1, table.maxSelectionColumn, true).getHeight()) { rowOffset = 0; } } // get column distance if (table.minSelectionColumn > 0 && table.draggingToColumn < table.minSelectionColumn) { colOffset = mouseX - (int) selRect.getX(); if (-colOffset < 0.5 * table.getCellRect( table.minSelectionRow, table.minSelectionColumn - 1, true) .getWidth()) { colOffset = 0; } } else if (table.maxSelectionColumn < app .getMaxSpreadsheetColumnsVisible() && table.draggingToColumn > table.maxSelectionColumn) { colOffset = mouseX - ((int) selRect.getX() + (int) selRect .getWidth()); if (colOffset < 0.5 * table.getCellRect( table.maxSelectionRow, table.maxSelectionColumn + 1, true) .getWidth()) { colOffset = 0; } } if (rowOffset == 0 && colOffset == 0) { table.draggingToColumn = -1; table.draggingToRow = -1; } else if (Math.abs(rowOffset) > Math.abs(colOffset)) { table.draggingToRow = mouseCell.y; table.draggingToColumn = (colOffset > 0) ? table.maxSelectionColumn : table.minSelectionColumn; } else { table.draggingToColumn = mouseCell.x; table.draggingToRow = (rowOffset > 0) ? table.maxSelectionRow : table.minSelectionRow; } table.repaint(); } // handle ctrl-select dragging of cell blocks else { /* * TODO if (e.isControlDown()) { * table.handleControlDragSelect(e); } */ } } } if (eConsumed) { return; } // MyTable's default listeners follow, they should be simulated in // Web e.g. here // change selection if right click is outside current selection if (point.getY() != table.leadSelectionRow || point.getX() != table.leadSelectionColumn) { // switch to cell selection mode if (point.getY() >= 0 && point.getX() >= 0) { changeSelection(point, true); table.repaint(); } } } else { // MOVE, NO DRAG if (table.isEditing()) { return; } // get GeoElement at mouse location int row = point.getY();// ?//table.rowAtPoint(e.getPoint()); int col = point.getX();// ?//table.columnAtPoint(e.getPoint()); GeoElement geo = (GeoElement) model.getValueAt(row, col); // set tooltip with geo's description if (geo != null & view.getAllowToolTips()) { table.setToolTipText(geo.getLongDescriptionHTML(true, true)); } else { table.setToolTipText(null); } updateTableIsOverDot(event); } } private void updateTableIsOverDot(DomEvent<?> event) { // check if over the dragging dot and update accordingly GPoint point = getPixelFromEvent(event); GPoint maxPoint = table.getMaxSelectionPixel(false); if (maxPoint != null) { int dotX = maxPoint.getX(); int dotY = maxPoint.getY(); int s = MyTableW.DOT_SIZE + 2; if (EventUtil.isTouchEvent(event)) { s += 4; } Rectangle2D dotRect = new Rectangle2D.Double(dotX - s / 2, dotY - s / 2, s, s); boolean overDot = dotRect.contains(point.getX(), point.getY()); if (table.isOverDot != overDot) { table.isOverDot = overDot; if (table.showCanDragBlueDot()) { table.repaint(); } } } } }