package org.geogebra.web.html5.euclidian; import java.util.LinkedList; import org.geogebra.common.euclidian.EuclidianConstants; import org.geogebra.common.euclidian.EuclidianController; import org.geogebra.common.euclidian.controller.MouseTouchGestureController; import org.geogebra.common.euclidian.event.AbstractEvent; import org.geogebra.common.euclidian.event.PointerEventType; import org.geogebra.common.kernel.geos.GeoElement; import org.geogebra.common.kernel.geos.GeoList; import org.geogebra.common.util.debug.GeoGebraProfiler; import org.geogebra.common.util.debug.Log; import org.geogebra.web.html5.Browser; import org.geogebra.web.html5.event.HasOffsets; import org.geogebra.web.html5.event.PointerEvent; import org.geogebra.web.html5.event.ZeroOffset; import org.geogebra.web.html5.gui.inputfield.AutoCompleteTextFieldW; import org.geogebra.web.html5.gui.tooltip.ToolTipManagerW; 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 com.google.gwt.core.client.JsArray; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.dom.client.Touch; import com.google.gwt.event.dom.client.GestureChangeEvent; import com.google.gwt.event.dom.client.GestureEndEvent; import com.google.gwt.event.dom.client.GestureStartEvent; import com.google.gwt.event.dom.client.MouseDownEvent; import com.google.gwt.event.dom.client.MouseMoveEvent; import com.google.gwt.event.dom.client.MouseOutEvent; import com.google.gwt.event.dom.client.MouseOverEvent; import com.google.gwt.event.dom.client.MouseUpEvent; import com.google.gwt.event.dom.client.MouseWheelEvent; import com.google.gwt.event.dom.client.TouchCancelEvent; import com.google.gwt.event.dom.client.TouchEndEvent; import com.google.gwt.event.dom.client.TouchMoveEvent; import com.google.gwt.event.dom.client.TouchStartEvent; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.Window; public class MouseTouchGestureControllerW extends MouseTouchGestureController implements HasOffsets { private AppW app; private long lastMoveEvent = 0; private PointerEvent waitingTouchMove = null; private PointerEvent waitingMouseMove = null; public EnvironmentStyleW style = new EnvironmentStyleW(); /** * Threshold for the selection rectangle distance squared (10 pixel circle) */ public final static double SELECTION_RECT_THRESHOLD_SQR = 200.0; public final static double FREEHAND_MODE_THRESHOLD_SQR = 200.0; private int DELAY_UNTIL_MOVE_FINISH = 150; /** * flag for blocking the scaling of the axes */ protected boolean moveAxesAllowed = true; private LongTouchManager longTouchManager; public EnvironmentStyleW getEnvironmentStyle() { return style; } /** * recalculates cached styles concerning browser environment */ public void calculateEnvironment() { if (ec.getView() == null) { return; } style = new EnvironmentStyleW(); style.setxOffset(getEnvXoffset()); style.setyOffset(getEnvYoffset()); style.setScaleX(app.getArticleElement().getScaleX()); style.setScaleY(app.getArticleElement().getScaleY()); style.setScrollLeft(Window.getScrollLeft()); style.setScrollTop(Window.getScrollTop()); ec.getView().setPixelRatio(app.getPixelRatio()); } private double getEnvWidthScale() { if (ec.getView() == null) { return 1; } EuclidianViewWInterface v = (EuclidianViewWInterface) ec.getView(); if (v.getG2P().getOffsetWidth() != 0) { return v.getG2P().getCoordinateSpaceWidth() / (double) v.getG2P().getOffsetWidth(); } return 0; } private double getEnvHeightScale() { EuclidianViewWInterface v = (EuclidianViewWInterface) ec.getView(); if (v.getG2P().getOffsetHeight() != 0) { return v.getG2P().getCoordinateSpaceHeight() / (double) v.getG2P().getOffsetHeight(); } return 0; } private int getEnvXoffset() { // return EuclidianViewXOffset; // the former solution doesn't update on scrolling return (((EuclidianViewWInterface) ec.getView()).getAbsoluteLeft() - Window.getScrollLeft()); } // private int EuclidianViewXOffset; // private int EuclidianViewYOffset; /** * @return offset to get correct getY() in mouseEvents */ private int getEnvYoffset() { // return EuclidianViewYOffset; // the former solution doesn't update on scrolling return ((EuclidianViewWInterface) ec.getView()).getAbsoluteTop() - Window.getScrollTop(); } private boolean EuclidianOffsetsInited = false; public boolean isOffsetsUpToDate() { return EuclidianOffsetsInited; } private Timer repaintTimer = new Timer() { @Override public void run() { moveIfWaiting(); } }; public void moveIfWaiting() { long time = System.currentTimeMillis(); if (this.waitingMouseMove != null) { GeoGebraProfiler.decrementMoveEventsIgnored(); this.onMouseMoveNow(waitingMouseMove, time, false); return; } if (this.waitingTouchMove != null) { GeoGebraProfiler.decrementMoveEventsIgnored(); this.onTouchMoveNow(waitingTouchMove, time, false); } } public MouseTouchGestureControllerW(AppW app, EuclidianController ec) { super(app, ec); this.app = app; Window.addWindowScrollHandler(new Window.ScrollHandler() { @Override public void onWindowScroll(Window.ScrollEvent event) { calculateEnvironment(); } }); app.addWindowResizeListener(this); longTouchManager = LongTouchManager.getInstance(); } public void handleLongTouch(int x, int y) { Log.debug("LONG TOUCH"); PointerEvent event = new PointerEvent(x, y, PointerEventType.TOUCH, ZeroOffset.instance); event.setIsRightClick(true); ec.wrapMouseReleased(event); } public void onGestureChange(GestureChangeEvent event) { // AbstractEvent e = // geogebra.web.euclidian.event.TouchEvent.wrapEvent(event.getNativeEvent()); // to not move the canvas (later some sophisticated handling must be // find out) // event.preventDefault(); // event.stopPropagation(); } public void onGestureEnd(GestureEndEvent event) { // AbstractEvent e = // geogebra.web.euclidian.event.TouchEvent.wrapEvent(event.getNativeEvent()); // to not move the canvas (later some sophisticated handling must be // find out) // event.preventDefault(); // event.stopPropagation(); } public void onGestureStart(GestureStartEvent event) { // AbstractEvent e = // geogebra.web.euclidian.event.TouchEvent.wrapEvent(event.getNativeEvent()); // to not move the canvas (later some sophisticated handling must be // find out) // event.preventDefault(); // event.stopPropagation(); } public void onTouchCancel(TouchCancelEvent event) { // AbstractEvent e = // geogebra.web.euclidian.event.TouchEvent.wrapEvent(event.getNativeEvent()); Log.debug(event.getAssociatedType().getName()); } public void onTouchMove(TouchMoveEvent event) { GeoGebraProfiler.incrementDrags(); long time = System.currentTimeMillis(); JsArray<Touch> targets = event.getTargetTouches(); event.stopPropagation(); event.preventDefault(); if (targets.length() == 1 && !ignoreEvent) { if (time < this.lastMoveEvent + EuclidianViewW.DELAY_BETWEEN_MOVE_EVENTS) { PointerEvent e = PointerEvent.wrapEvent( targets.get(targets.length() - 1), this, event.getRelativeElement()); boolean wasWaiting = waitingTouchMove != null || waitingMouseMove != null; this.waitingTouchMove = e; this.waitingMouseMove = null; GeoGebraProfiler.incrementMoveEventsIgnored(); if (wasWaiting) { this.repaintTimer .schedule(DELAY_UNTIL_MOVE_FINISH); } return; } PointerEvent e = PointerEvent.wrapEvent( targets.get(targets.length() - 1), this, event.getRelativeElement()); if (!ec.draggingBeyondThreshold) { if (ec.isDraggingBeyondThreshold(1)) { longTouchManager.rescheduleTimerIfRunning( (LongTouchHandler) ec, e.getX(), e.getY(), false); } } else { longTouchManager.cancelTimer(); } onTouchMoveNow(e, time, true); } else if (targets.length() == 2 && app.isShiftDragZoomEnabled()) { longTouchManager.cancelTimer(); twoTouchMove(targets.get(0), targets.get(1)); } else { longTouchManager.cancelTimer(); } CancelEventTimer.touchEventOccured(); } public void twoTouchMove(Touch touch, Touch touch2) { AbstractEvent first = PointerEvent.wrapEvent(touch, this); AbstractEvent second = PointerEvent.wrapEvent(touch2, this); ec.twoTouchMove(first.getX(), first.getY(), second.getX(), second.getY()); first.release(); second.release(); } private static double distance(final AbstractEvent t1, final AbstractEvent t2) { return Math.sqrt(Math.pow(t1.getX() - t2.getX(), 2) + Math.pow(t1.getY() - t2.getY(), 2)); } public void onTouchMoveNow(PointerEvent event, long time, boolean startCapture) { this.lastMoveEvent = time; // in SMART we actually get move events even if mouse button is up ... if (!dragModeMustBeSelected) { ec.wrapMouseMoved(event); } else { ec.wrapMouseDragged(event, startCapture); } this.waitingTouchMove = null; this.waitingMouseMove = null; int dragTime = (int) (System.currentTimeMillis() - time); GeoGebraProfiler.incrementDragTime(dragTime); if (dragTime > DELAY_UNTIL_MOVE_FINISH) { DELAY_UNTIL_MOVE_FINISH = dragTime + 10; } moveCounter++; } /** * ignore events after first touchEnd of a multi touch event */ private boolean ignoreEvent = false; public void onTouchEnd(TouchEndEvent event) { Event.releaseCapture(event.getRelativeElement()); dragModeMustBeSelected = false; if (moveCounter < 2) { ec.resetModeAfterFreehand(); } this.moveIfWaiting(); resetDelay(); event.stopPropagation(); longTouchManager.cancelTimer(); if (!comboBoxHit()) { event.preventDefault(); } if (event.getTouches().length() == 0 && !ignoreEvent) { // mouseLoc was already adjusted to the EVs coords, do not use // offset again ec.wrapMouseReleased(new PointerEvent(ec.mouseLoc.x, ec.mouseLoc.y, PointerEventType.TOUCH, ZeroOffset.instance)); } else { // multitouch-event // ignore next touchMove and touchEnd events with one touch ignoreEvent = true; } CancelEventTimer.touchEventOccured(); ec.resetModeAfterFreehand(); } public void onTouchStart(TouchStartEvent event) { JsArray<Touch> targets = event.getTargetTouches(); calculateEnvironment(); final boolean inputBoxFocused = false; ec.setDefaultEventType(PointerEventType.TOUCH, true); if (targets.length() == 1) { AbstractEvent e = PointerEvent.wrapEvent(targets.get(0), this); if (ec.getMode() == EuclidianConstants.MODE_MOVE) { longTouchManager.scheduleTimer((LongTouchHandler) ec, e.getX(), e.getY()); } // inputBoxFocused = ec.textfieldJustFocusedW(e.getX(), e.getY(), // e.getType()); onPointerEventStart(e); } else if (targets.length() == 2) { longTouchManager.cancelTimer(); twoTouchStart(targets.get(0), targets.get(1)); } else { longTouchManager.cancelTimer(); } if (!inputBoxFocused) { preventTouchIfNeeded(event); } CancelEventTimer.touchEventOccured(); moveCounter = 0; ignoreEvent = false; } public void preventTouchIfNeeded(TouchStartEvent event) { if ((!ec.isTextfieldHasFocus()) && (!comboBoxHit())) { event.preventDefault(); } } public void twoTouchStart(Touch touch, Touch touch2) { calculateEnvironment(); AbstractEvent first = PointerEvent.wrapEvent(touch, this); AbstractEvent second = PointerEvent.wrapEvent(touch2, this); ec.twoTouchStart(first.getX(), first.getY(), second.getX(), second.getY()); first.release(); second.release(); } private boolean dragModeMustBeSelected = false; private int deltaSum = 0; private int moveCounter = 0; private boolean dragModeIsRightClick = false; public void onMouseWheel(MouseWheelEvent event) { // don't want to roll the scrollbar double delta = event.getDeltaY(); // we are on device where many small scrolls come, we want to merge them int x = mouseEventX(event.getClientX() - style.getxOffset()); int y = mouseEventX(event.getClientY() - style.getyOffset()); boolean shiftOrMeta = event.isShiftKeyDown() || event.isMetaKeyDown(); if (delta == 0) { deltaSum += getNativeDelta(event.getNativeEvent()); if (Math.abs(deltaSum) > 40) { double ds = deltaSum; deltaSum = 0; ec.wrapMouseWheelMoved(x, y, ds, shiftOrMeta, event.isAltKeyDown()); } // normal scrolling } else { deltaSum = 0; ec.wrapMouseWheelMoved(x, y, delta, shiftOrMeta, event.isAltKeyDown()); } if (ec.allowMouseWheel(shiftOrMeta)) { event.preventDefault(); } } private native double getNativeDelta(NativeEvent evt) /*-{ return -evt.wheelDelta; }-*/; public void onMouseOver(MouseOverEvent event) { ec.wrapMouseEntered(); } public void onMouseOut(MouseOutEvent event) { // cancel repaint to avoid closing newly opened tooltips repaintTimer.cancel(); // hide dialogs if they are open int x = event.getClientX() + Window.getScrollLeft(); int y = event.getClientY() + Window.getScrollTop(); // why scrollLeft & // scrollTop; see // ticket #4049 int ex = ((EuclidianViewWInterface) ec.getView()).getAbsoluteLeft(); int ey = ((EuclidianViewWInterface) ec.getView()).getAbsoluteTop(); int eWidth = ((EuclidianViewWInterface) ec.getView()).getWidth(); int eHeight = ((EuclidianViewWInterface) ec.getView()).getHeight(); if ((x < ex || x > ex + eWidth) || (y < ey || y > ey + eHeight)) { ToolTipManagerW.sharedInstance().hideToolTip(); } ((EuclidianViewWInterface) ec.getView()).resetPointerEventHandler(); AbstractEvent e = PointerEvent.wrapEvent(event, this); ec.wrapMouseExited(e); e.release(); } public void onMouseMove(MouseMoveEvent event) { if (CancelEventTimer.cancelMouseEvent()) { return; } if (ec.isExternalHandling()) { return; } PointerEvent e = PointerEvent.wrapEvent(event, this); event.preventDefault(); GeoGebraProfiler.incrementDrags(); long time = System.currentTimeMillis(); if (time < this.lastMoveEvent + EuclidianViewW.DELAY_BETWEEN_MOVE_EVENTS) { boolean wasWaiting = waitingTouchMove != null || waitingMouseMove != null; this.waitingMouseMove = e; this.setWaitingTouchMove(null); GeoGebraProfiler.incrementMoveEventsIgnored(); if (wasWaiting) { this.repaintTimer .schedule(DELAY_UNTIL_MOVE_FINISH); } if (ec.getView().getMode() != EuclidianConstants.MODE_FREEHAND_SHAPE && ec.getView().getMode() != EuclidianConstants.MODE_PEN) { return; } } onMouseMoveNow(e, time, true); } private void setWaitingTouchMove(PointerEvent o) { waitingTouchMove = o; } public void onMouseMoveNow(PointerEvent event, long time, boolean startCapture) { this.lastMoveEvent = time; if (!dragModeMustBeSelected) { ec.wrapMouseMoved(event); } else { event.setIsRightClick(dragModeIsRightClick); ec.wrapMouseDragged(event, startCapture); } event.release(); this.waitingMouseMove = null; this.waitingTouchMove = null; int dragTime = (int) (System.currentTimeMillis() - time); GeoGebraProfiler.incrementDragTime(dragTime); if (dragTime > DELAY_UNTIL_MOVE_FINISH) { DELAY_UNTIL_MOVE_FINISH = dragTime + 10; } moveCounter++; } public void onMouseUp(MouseUpEvent event) { Event.releaseCapture(event.getRelativeElement()); if (CancelEventTimer.cancelMouseEvent()) { return; } event.preventDefault(); AbstractEvent e = PointerEvent.wrapEvent(event, this); onPointerEventEnd(e); // // if (elementCreated) { // ec.toolCompleted(); // } } public void onPointerEventEnd(AbstractEvent e) { if (moveCounter < 2) { ec.resetModeAfterFreehand(); } this.moveIfWaiting(); resetDelay(); dragModeMustBeSelected = false; // hide dialogs if they are open // but don't hide context menu if we just opened it via long tap in IE if (ec.getDefaultEventType() == PointerEventType.MOUSE && app.getGuiManager() != null) { app.getGuiManager().removePopup(); } ec.wrapMouseReleased(e); e.release(); // boolean elementCreated = ec.pen != null // && ec.pen.getCreatedShape() != null; ec.resetModeAfterFreehand(); } public void onMouseDown(MouseDownEvent event) { deltaSum = 0; if (CancelEventTimer.cancelMouseEvent()) { return; } // No prevent default here: make sure keyboard focus goes to canvas AbstractEvent e = PointerEvent.wrapEvent(event, this); ec.setDefaultEventType(PointerEventType.MOUSE, true); ec.onPointerEventStart(e); moveCounter = 0; ignoreEvent = false; } public void onPointerEventStart(AbstractEvent event) { if ((!AutoCompleteTextFieldW.isShowSymbolButtonFocused()) && (!ec.isTextfieldHasFocus())) { dragModeMustBeSelected = true; dragModeIsRightClick = event.isRightClick(); } ec.wrapMousePressed(event); // hide PopUp if no hits was found. if (ec.getView().getHits().isEmpty() && ec.getView().hasStyleBar()) { ec.getView().getStyleBar().hidePopups(); } if (!event.isRightClick()) { ec.prepareModeForFreehand(); } event.release(); } private boolean comboBoxHit() { if (ec.getView().getHits() == null) { return false; } int i = 0; while (i < ec.getView().getHits().size()) { GeoElement hit = ec.getView().getHits().get(i++); if (hit instanceof GeoList && ((GeoList) hit).drawAsComboBox()) { return true; } } return false; } public void initToolTipManager() { // set tooltip manager ToolTipManagerW.sharedInstance(); // ttm.setInitialDelay(defaultInitialDelay / 2); // ttm.setEnabled((AppW.getAllowToolTips()); } public void resetToolTipManager() { // TODO Auto-generated method stub } public boolean hitResetIcon() { return app.showResetIcon() && ((ec.mouseLoc.y < 20) && (ec.mouseLoc.x > (ec.getView() .getViewWidth() - 18))); } private LinkedList<PointerEvent> mousePool = new LinkedList<PointerEvent>(); @Override public LinkedList<PointerEvent> getMouseEventPool() { return mousePool; } private LinkedList<PointerEvent> touchPool = new LinkedList<PointerEvent>(); private boolean comboboxFocused; @Override public LinkedList<PointerEvent> getTouchEventPool() { return touchPool; } protected boolean textfieldJustFocusedW(int x, int y, PointerEventType type) { return ec.getView().textfieldClicked(x, y, type) || isComboboxFocused(); } public boolean isComboboxFocused() { return this.comboboxFocused; } public void setComboboxFocused(boolean flag) { this.comboboxFocused = flag; } @Override public int touchEventX(int clientX) { if (app.getLAF() != null && app.getLAF().isSmart()) { return mouseEventX(clientX - style.getxOffset()); } // IE touch events are mouse events return Browser.supportsPointerEvents(false) ? mouseEventX(clientX) : mouseEventX(clientX - style.getxOffset()); } @Override public int touchEventY(int clientY) { if (app.getLAF() != null && app.getLAF().isSmart()) { return mouseEventY(clientY - style.getyOffset()); } // IE touch events are mouse events return Browser.supportsPointerEvents(false) ? mouseEventY(clientY) : mouseEventY(clientY - style.getyOffset()); } /** * @return the multiplier that must be used to multiply the native event * coordinates */ public double getScaleXMultiplier() { return style.getScaleXMultiplier(); } /** * @return the multiplier that must be used to multiply the native event * coordinates */ public double getScaleYMultiplier() { return style.getScaleYMultiplier(); } @Override public int mouseEventX(int clientX) { return (int) Math.round((clientX) * (1 / style.getScaleX())); } @Override public int mouseEventY(int clientY) { return (int) Math.round((clientY) * (1 / style.getScaleY())); } @Override public int getEvID() { return ec.getView().getViewID(); } @Override public PointerEventType getDefaultEventType() { return ec.getDefaultEventType(); } public LongTouchManager getLongTouchManager() { return longTouchManager; } public void resetDelay() { DELAY_UNTIL_MOVE_FINISH = 150; } public void closePopups() { app.onUnhandledClick(); app.closePerspectivesPopup(); app.closePopups(); } }