/* * Copyright 2009 Fred Sauer * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.allen_sauer.gwt.dnd.client; import com.google.gwt.core.client.JsArray; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.BorderStyle; import com.google.gwt.dom.client.Touch; import com.google.gwt.event.dom.client.HasMouseDownHandlers; import com.google.gwt.event.dom.client.HasTouchStartHandlers; import com.google.gwt.event.dom.client.HumanInputEvent; 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.TouchCancelEvent; import com.google.gwt.event.dom.client.TouchCancelHandler; import com.google.gwt.event.dom.client.TouchEndEvent; import com.google.gwt.event.dom.client.TouchEndHandler; import com.google.gwt.event.dom.client.TouchEvent; 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.event.shared.HandlerRegistration; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.ui.FocusPanel; import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.PopupPanel; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.Widget; import com.allen_sauer.gwt.dnd.client.util.DOMUtil; import com.allen_sauer.gwt.dnd.client.util.Location; import com.allen_sauer.gwt.dnd.client.util.WidgetLocation; import java.util.HashMap; /** * Implementation helper class which handles mouse events for all draggable widgets for a given * {@link DragController}. */ class MouseDragHandler implements MouseMoveHandler, MouseDownHandler, MouseUpHandler, TouchStartHandler, TouchMoveHandler, TouchEndHandler, TouchCancelHandler { private class RegisteredDraggable { private final Widget dragable; private HandlerRegistration mouseDownHandlerRegistration; private HandlerRegistration touchStartHandlerRegistration; RegisteredDraggable(Widget dragable, Widget dragHandle) { this.dragable = dragable; if (dragHandle instanceof HasTouchStartHandlers) { touchStartHandlerRegistration = ((HasTouchStartHandlers) dragHandle).addTouchStartHandler(MouseDragHandler.this); } if (dragHandle instanceof HasMouseDownHandlers) { mouseDownHandlerRegistration = ((HasMouseDownHandlers) dragHandle).addMouseDownHandler(MouseDragHandler.this); } } Widget getDragable() { return dragable; } HandlerRegistration getMouseDownHandlerRegistration() { return mouseDownHandlerRegistration; } HandlerRegistration getTouchStartHandlerRegistration() { return touchStartHandlerRegistration; } } private static final int ACTIVELY_DRAGGING = 3; private static final int DRAGGING_NO_MOVEMENT_YET = 2; private static Widget mouseDownWidget; private static final int NOT_DRAGGING = 1; private static boolean supportsTouchEvents; private FocusPanel capturingWidget; private final DragContext context; private int dragging = NOT_DRAGGING; private HashMap<Widget, RegisteredDraggable> dragHandleMap = new HashMap<Widget, RegisteredDraggable>(); private int mouseDownOffsetX; private int mouseDownOffsetY; private int mouseDownPageOffsetX; private int mouseDownPageOffsetY; MouseDragHandler(DragContext context) { this.context = context; initCapturingWidget(); } @Override public void onMouseDown(MouseDownEvent event) { // ******************************************************************* // Note: the draggable (or its draghandle) receives mouse down events, // but the capturing widget will receive mouse move/up events. // ******************************************************************* if (supportsTouchEvents) { return; } if (dragging == ACTIVELY_DRAGGING || dragging == DRAGGING_NO_MOVEMENT_YET) { // Ignore additional mouse buttons depressed while still dragging return; } Widget sender = (Widget) event.getSource(); int x = event.getX(); int y = event.getY(); int button = event.getNativeButton(); if (button != NativeEvent.BUTTON_LEFT) { return; } if (mouseDownWidget != null) { // For multiple overlapping draggable widgets, ignore all but first onMouseDown return; } // mouse down (not first mouse move) determines draggable widget mouseDownWidget = sender; context.draggable = dragHandleMap.get(mouseDownWidget).getDragable(); assert context.draggable != null; if (!toggleKey(event) && !context.selectedWidgets.contains(context.draggable)) { context.dragController.clearSelection(); context.dragController.toggleSelection(context.draggable); } // prevent browser image dragging in Firefox et al. //if (mouseDownWidget instanceof Image) { event.preventDefault(); //} mouseDownOffsetX = x; mouseDownOffsetY = y; WidgetLocation loc1 = new WidgetLocation(mouseDownWidget, null); if (mouseDownWidget != context.draggable) { WidgetLocation loc2 = new WidgetLocation(context.draggable, null); mouseDownOffsetX += loc1.getLeft() - loc2.getLeft(); mouseDownOffsetY += loc1.getTop() - loc2.getTop(); } if (context.dragController.getBehaviorDragStartSensitivity() == 0 && !toggleKey(event)) { // set context.mouseX/Y before startDragging() is called context.mouseX = x + loc1.getLeft(); context.mouseY = y + loc1.getTop(); startDragging(); if (dragging == NOT_DRAGGING) { return; } actualMove(context.mouseX, context.mouseY); } else { mouseDownPageOffsetX = mouseDownOffsetX + loc1.getLeft(); mouseDownPageOffsetY = mouseDownOffsetY + loc1.getTop(); startCapturing(); } } @Override public void onMouseMove(MouseMoveEvent event) { // ******************************************************************* // Note: the draggable (or its draghandle) receives mouse down events, // but the capturing widget will receive mouse move/up events. // ******************************************************************* if (supportsTouchEvents) { return; } Widget sender = (Widget) event.getSource(); Element elem = sender.getElement(); // TODO optimize for the fact that elem is at (0,0) int x = event.getRelativeX(elem); int y = event.getRelativeY(elem); if (dragging == ACTIVELY_DRAGGING || dragging == DRAGGING_NO_MOVEMENT_YET) { dragging = ACTIVELY_DRAGGING; } else { if (mouseDownWidget != null) { if (Math.max(Math.abs(x - mouseDownPageOffsetX), Math.abs(y - mouseDownPageOffsetY)) >= context.dragController.getBehaviorDragStartSensitivity()) { // cancel selection when drag sensitivity >= 2 on webkit maybeCancelDocumentSelections(); if (!context.selectedWidgets.contains(context.draggable)) { context.dragController.toggleSelection(context.draggable); } // set context.mouseX/Y before startDragging() is called Location location = new WidgetLocation(mouseDownWidget, null); context.mouseX = mouseDownOffsetX + location.getLeft(); context.mouseY = mouseDownOffsetY + location.getTop(); startDragging(); } } if (dragging == NOT_DRAGGING) { return; } } // proceed with the actual drag actualMove(x, y); } private void maybeCancelDocumentSelections() { if (context.dragController.getBehaviorCancelDocumentSelections()) { Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { DOMUtil.cancelAllDocumentSelections(); } }); } } @Override public void onMouseUp(MouseUpEvent event) { // ******************************************************************* // Note: the draggable (or its draghandle) receives mouse down events, // but the capturing widget will receive mouse move/up events. // ******************************************************************* if (supportsTouchEvents) { return; } Widget sender = (Widget) event.getSource(); Element elem = sender.getElement(); // TODO optimize for the fact that elem is at (0,0) int x = event.getRelativeX(elem); int y = event.getRelativeY(elem); int button = event.getNativeButton(); if (button != NativeEvent.BUTTON_LEFT) { return; } // in case mouse down occurred elsewhere if (mouseDownWidget == null) { return; } try { if (dragging == NOT_DRAGGING) { doSelectionToggle(event); return; } // Proceed with the drop try { synthesizeAsyncMouseUp(event); drop(x, y); if (dragging != ACTIVELY_DRAGGING) { doSelectionToggle(event); } } finally { dragEndCleanup(); } } finally { mouseDownWidget = null; dragEndCleanup(); } } private void synthesizeAsyncTouchEnd(TouchEndEvent event) { final Element elem = mouseDownWidget.getElement(); NativeEvent n = event.getNativeEvent(); // TODO extract these properties from the original event final boolean bubbles = true; final boolean cancelable = true; final int detail = 0; final boolean ctrlKey = n.getCtrlKey(); final boolean altKey = n.getAltKey(); final boolean shiftKey = n.getShiftKey(); final boolean metaKey = n.getMetaKey(); final JsArray<Touch> changedTouches = n.getChangedTouches(); Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { // TODO determine if we need to set additional event properties elem.dispatchEvent(DOMUtil.createTouchEndEvent(bubbles, cancelable, detail, ctrlKey, altKey, shiftKey, metaKey, changedTouches)); } }); } private void synthesizeAsyncMouseUp(MouseUpEvent event) { final Element elem = mouseDownWidget.getElement(); NativeEvent n = event.getNativeEvent(); // One click, see https://developer.mozilla.org/en-US/docs/DOM/event.detail final int detail = 1; final int screenX = n.getScreenX(); final int screenY = n.getScreenY(); final int clientX = n.getClientX(); final int clientY = n.getClientY(); final boolean ctrlKey = n.getCtrlKey(); final boolean altKey = n.getAltKey(); final boolean shiftKey = n.getShiftKey(); final boolean metaKey = n.getMetaKey(); final int button = n.getButton(); Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { // TODO determine if we need to set additional event properties elem.dispatchEvent(Document.get().createMouseUpEvent(detail, screenX, screenY, clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, button)); } }); } @Override public void onTouchCancel(TouchCancelEvent event) { // ******************************************************************** // Note: the draggable (or its draghandle) receives touch start events, // but the capturing widget will receive touch move/end/cancel events. // ******************************************************************** onTouchEndorCancel(event); } @Override public void onTouchEnd(TouchEndEvent event) { // ******************************************************************** // Note: the draggable (or its draghandle) receives touch start events, // but the capturing widget will receive touch move/end/cancel events. // ******************************************************************** synthesizeAsyncTouchEnd(event); onTouchEndorCancel(event); } @Override public void onTouchMove(TouchMoveEvent event) { // ******************************************************************** // Note: the draggable (or its draghandle) receives touch start events, // but the capturing widget will receive touch move/end/cancel events. // ******************************************************************** if (event.getTouches().length() != 1) { // ignore multiple fingers for now return; } Widget sender = (Widget) event.getSource(); Element elem = sender.getElement(); // TODO optimize for the fact that elem is at (0,0) int x = event.getTouches().get(0).getRelativeX(elem); int y = event.getTouches().get(0).getRelativeY(elem); if (dragging == ACTIVELY_DRAGGING || dragging == DRAGGING_NO_MOVEMENT_YET) { dragging = ACTIVELY_DRAGGING; } else { if (mouseDownWidget != null) { if (Math.max(Math.abs(x - mouseDownOffsetX), Math.abs(y - mouseDownOffsetY)) >= context.dragController.getBehaviorDragStartSensitivity()) { if (!context.selectedWidgets.contains(context.draggable)) { context.dragController.toggleSelection(context.draggable); } // set context.mouseX/Y before startDragging() is called Location location = new WidgetLocation(mouseDownWidget, null); context.mouseX = mouseDownOffsetX + location.getLeft(); context.mouseY = mouseDownOffsetY + location.getTop(); // adjust (x,y) to be relative to capturingWidget at (0,0) // so that context.desiredDraggableX/Y is valid x += location.getLeft(); y += location.getTop(); startDragging(); } } if (dragging == NOT_DRAGGING) { return; } } // prevent default page content drag event.preventDefault(); // proceed with the actual drag actualMove(x, y); } @Override public void onTouchStart(TouchStartEvent event) { // ******************************************************************** // Note: the draggable (or its draghandle) receives touch start events, // but the capturing widget will receive touch move/end/cancel events. // ******************************************************************** supportsTouchEvents = true; if (event.getTouches().length() != 1) { // ignore multiple fingers for now return; } Widget sender = (Widget) event.getSource(); int x = event.getTouches().get(0).getRelativeX(event.getRelativeElement()); int y = event.getTouches().get(0).getRelativeY(event.getRelativeElement()); // mouse down (not first mouse move) determines draggable widget mouseDownWidget = sender; context.draggable = dragHandleMap.get(mouseDownWidget).getDragable(); assert context.draggable != null; context.dragController.clearSelection(); context.dragController.toggleSelection(context.draggable); mouseDownOffsetX = x; mouseDownOffsetY = y; WidgetLocation loc1 = new WidgetLocation(mouseDownWidget, null); if (mouseDownWidget != context.draggable) { WidgetLocation loc2 = new WidgetLocation(context.draggable, null); mouseDownOffsetX += loc1.getLeft() - loc2.getLeft(); mouseDownOffsetY += loc1.getTop() - loc2.getTop(); } if (context.dragController.getBehaviorDragStartSensitivity() == 0 && !toggleKey(event)) { // set context.mouseX/Y before startDragging() is called context.mouseX = x + loc1.getLeft(); context.mouseY = y + loc1.getTop(); startDragging(); if (dragging == NOT_DRAGGING) { return; } actualMove(context.mouseX, context.mouseY); } else { startCapturing(); } } void actualMove(int x, int y) { context.mouseX = x; context.mouseY = y; context.desiredDraggableX = x - mouseDownOffsetX; context.desiredDraggableY = y - mouseDownOffsetY; context.dragController.dragMove(); } void makeDraggable(Widget draggable, Widget dragHandle) { if (draggable instanceof PopupPanel) { DOMUtil.reportFatalAndThrowRuntimeException( "PopupPanel (and its subclasses) cannot be made draggable; See http://code.google.com/p/gwt-dnd/issues/detail?id=43"); } try { RegisteredDraggable registeredDraggable = new RegisteredDraggable(draggable, dragHandle); dragHandleMap.put(dragHandle, registeredDraggable); } catch (Exception ex) { throw new RuntimeException( "dragHandle must implement HasMouseDownHandlers to be draggable", ex); } } void makeNotDraggable(Widget dragHandle) { RegisteredDraggable registeredDraggable = dragHandleMap.remove(dragHandle); if (registeredDraggable == null) { throw new RuntimeException("dragHandle was not draggable"); } registeredDraggable.getMouseDownHandlerRegistration().removeHandler(); registeredDraggable.getTouchStartHandlerRegistration().removeHandler(); } private void doSelectionToggle(HumanInputEvent<?> event) { Widget widget = dragHandleMap.get(mouseDownWidget).getDragable(); assert widget != null; if (!toggleKey(event)) { context.dragController.clearSelection(); } context.dragController.toggleSelection(widget); } private void dragEndCleanup() { DOM.releaseCapture(capturingWidget.getElement()); capturingWidget.removeFromParent(); dragging = NOT_DRAGGING; context.dragEndCleanup(); } private void drop(int x, int y) { actualMove(x, y); // Does the DragController allow the drop? try { context.dragController.previewDragEnd(); } catch (VetoDragException ex) { context.vetoException = ex; } context.dragController.dragEnd(); } private void initCapturingWidget() { capturingWidget = new FocusPanel(); capturingWidget.addMouseMoveHandler(this); capturingWidget.addMouseUpHandler(this); capturingWidget.addTouchMoveHandler(this); capturingWidget.addTouchEndHandler(this); capturingWidget.addTouchCancelHandler(this); Style style = capturingWidget.getElement().getStyle(); // workaround for IE8 opacity http://code.google.com/p/google-web-toolkit/issues/detail?id=5538 style.setProperty("filter", "alpha(opacity=0)"); style.setOpacity(0); style.setZIndex(1000); style.setMargin(0, Style.Unit.PX); style.setBorderStyle(BorderStyle.NONE); style.setBackgroundColor("blue"); } private void onTouchEndorCancel(TouchEvent<?> event) { if (event.getTouches().length() != 0) { // ignore multiple fingers for now return; } // in case mouse down occurred elsewhere if (mouseDownWidget == null) { return; } try { if (dragging == NOT_DRAGGING) { doSelectionToggle(event); return; } // Proceed with the drop try { drop(context.mouseX, context.mouseY); if (dragging != ACTIVELY_DRAGGING) { doSelectionToggle(event); } } finally { dragEndCleanup(); } } finally { mouseDownWidget = null; dragEndCleanup(); } } private void startCapturing() { capturingWidget.setPixelSize(0, 0); RootPanel.get().add(capturingWidget, 0, 0); DOM.setCapture(capturingWidget.getElement()); } private void startDragging() { context.dragStartCleanup(); try { context.dragController.previewDragStart(); } catch (VetoDragException ex) { context.vetoException = ex; mouseDownWidget = null; dragEndCleanup(); return; } context.dragController.dragStart(); startCapturing(); capturingWidget.setPixelSize( RootPanel.get().getOffsetWidth(), RootPanel.get().getOffsetHeight()); dragging = DRAGGING_NO_MOVEMENT_YET; } private boolean toggleKey(HumanInputEvent<?> event) { return event.isControlKeyDown() || event.isMetaKeyDown(); } }