/*
* Copyright 2010 IT Mill Ltd.
*
* 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.vaadin.terminal.gwt.client.ui.dd;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.terminal.gwt.client.ApplicationConnection;
import com.vaadin.terminal.gwt.client.MouseEventDetails;
import com.vaadin.terminal.gwt.client.Paintable;
import com.vaadin.terminal.gwt.client.UIDL;
import com.vaadin.terminal.gwt.client.Util;
import com.vaadin.terminal.gwt.client.ValueMap;
/**
* Helper class to manage the state of drag and drop event on Vaadin client
* side. Can be used to implement most of the drag and drop operation
* automatically via cross-browser event preview method or just as a helper when
* implementing own low level drag and drop operation (like with HTML5 api).
* <p>
* Singleton. Only one drag and drop operation can be active anyways. Use
* {@link #get()} to get instance.
*
*/
public class VDragAndDropManager {
private final class DefaultDragAndDropEventHandler implements
NativePreviewHandler {
public void onPreviewNativeEvent(NativePreviewEvent event) {
NativeEvent nativeEvent = event.getNativeEvent();
int typeInt = event.getTypeInt();
if (typeInt == Event.ONKEYDOWN) {
int keyCode = event.getNativeEvent().getKeyCode();
if (keyCode == KeyCodes.KEY_ESCAPE) {
// end drag if ESC is hit
interruptDrag();
event.cancel();
event.getNativeEvent().preventDefault();
}
// no use for handling for any key down event
return;
}
currentDrag.setCurrentGwtEvent(nativeEvent);
updateDragImagePosition();
Element targetElement = Element.as(nativeEvent.getEventTarget());
if (dragElement != null && dragElement.isOrHasChild(targetElement)) {
// to detect the "real" target, hide dragelement temporary and
// use elementFromPoint
String display = dragElement.getStyle().getDisplay();
dragElement.getStyle().setDisplay(Display.NONE);
try {
int x = nativeEvent.getClientX();
int y = nativeEvent.getClientY();
// Util.browserDebugger();
targetElement = Util.getElementFromPoint(x, y);
if (targetElement == null) {
// ApplicationConnection.getConsole().log(
// "Event on dragImage, ignored");
event.cancel();
nativeEvent.stopPropagation();
return;
} else {
// ApplicationConnection.getConsole().log(
// "Event on dragImage, target changed");
// special handling for events over dragImage
// pretty much all events are mousemove althout below
// kind of happens mouseover
switch (typeInt) {
case Event.ONMOUSEOVER:
case Event.ONMOUSEOUT:
// ApplicationConnection
// .getConsole()
// .log(
// "IGNORING proxy image event, fired because of hack or not significant");
return;
case Event.ONMOUSEMOVE:
VDropHandler findDragTarget = findDragTarget(targetElement);
if (findDragTarget != currentDropHandler) {
// dragleave on old
if (currentDropHandler != null) {
currentDropHandler.dragLeave(currentDrag);
currentDrag.getDropDetails().clear();
serverCallback = null;
}
// dragenter on new
currentDropHandler = findDragTarget;
if (findDragTarget != null) {
// ApplicationConnection.getConsole().log(
// "DropHandler now"
// + currentDropHandler
// .getPaintable());
}
if (currentDropHandler != null) {
currentDrag
.setElementOver((com.google.gwt.user.client.Element) targetElement);
currentDropHandler.dragEnter(currentDrag);
}
} else if (findDragTarget != null) {
currentDrag
.setElementOver((com.google.gwt.user.client.Element) targetElement);
currentDropHandler.dragOver(currentDrag);
}
// prevent text selection on IE
nativeEvent.preventDefault();
return;
default:
// just update element over and let the actual
// handling code do the thing
// ApplicationConnection.getConsole().log(
// "Target just modified on "
// + event.getType());
currentDrag
.setElementOver((com.google.gwt.user.client.Element) targetElement);
break;
}
}
} catch (RuntimeException e) {
// ApplicationConnection.getConsole().log(
// "ERROR during elementFromPoint hack.");
throw e;
} finally {
dragElement.getStyle().setProperty("display", display);
}
}
switch (typeInt) {
case Event.ONMOUSEOVER:
VDropHandler target = findDragTarget(targetElement);
if (target != null && target != currentDropHandler) {
if (currentDropHandler != null) {
currentDropHandler.dragLeave(currentDrag);
currentDrag.getDropDetails().clear();
}
currentDropHandler = target;
// ApplicationConnection.getConsole().log(
// "DropHandler now"
// + currentDropHandler.getPaintable());
target.dragEnter(currentDrag);
} else if (target == null && currentDropHandler != null) {
// ApplicationConnection.getConsole().log("Invalid state!?");
currentDropHandler.dragLeave(currentDrag);
currentDrag.getDropDetails().clear();
currentDropHandler = null;
}
break;
case Event.ONMOUSEOUT:
Element relatedTarget = Element.as(nativeEvent
.getRelatedEventTarget());
VDropHandler newDragHanler = findDragTarget(relatedTarget);
if (dragElement != null
&& dragElement.isOrHasChild(relatedTarget)) {
// ApplicationConnection.getConsole().log(
// "Mouse out of dragImage, ignored");
return;
}
if (currentDropHandler != null
&& currentDropHandler != newDragHanler) {
currentDropHandler.dragLeave(currentDrag);
currentDrag.getDropDetails().clear();
currentDropHandler = null;
serverCallback = null;
}
break;
case Event.ONMOUSEMOVE:
if (currentDropHandler != null) {
currentDropHandler.dragOver(currentDrag);
}
nativeEvent.preventDefault();
break;
case Event.ONMOUSEUP:
endDrag();
break;
default:
break;
}
}
}
public enum DragEventType {
ENTER, LEAVE, OVER, DROP
}
private static final String DD_SERVICE = "DD";
private static VDragAndDropManager instance;
private HandlerRegistration handlerRegistration;
private VDragEvent currentDrag;
/**
* If dragging is currently on a drophandler, this field has reference to it
*/
private VDropHandler currentDropHandler;
public VDropHandler getCurrentDropHandler() {
return currentDropHandler;
}
/**
* If drag and drop operation is not handled by {@link VDragAndDropManager}s
* internal handler, this can be used to update current {@link VDropHandler}
* .
*
* @param currentDropHandler
*/
public void setCurrentDropHandler(VDropHandler currentDropHandler) {
this.currentDropHandler = currentDropHandler;
}
private VDragEventServerCallback serverCallback;
private HandlerRegistration deferredStartRegistration;
public static VDragAndDropManager get() {
if (instance == null) {
instance = new VDragAndDropManager();
}
return instance;
}
/* Singleton */
private VDragAndDropManager() {
}
private NativePreviewHandler defaultDragAndDropEventHandler = new DefaultDragAndDropEventHandler();
/**
* Flag to indicate if drag operation has really started or not. Null check
* of currentDrag field is not enough as a lazy start may be pending.
*/
private boolean isStarted;
/**
* This method is used to start Vaadin client side drag and drop operation.
* Operation may be started by virtually any Widget.
* <p>
* Cancels possible existing drag. TODO figure out if this is always a bug
* if one is active. Maybe a good and cheap lifesaver thought.
* <p>
* If possible, method automatically detects current {@link VDropHandler}
* and fires {@link VDropHandler#dragEnter(VDragEvent)} event on it.
* <p>
* May also be used to control the drag and drop operation. If this option
* is used, {@link VDropHandler} is searched on mouse events and appropriate
* methods on it called automatically.
*
* @param transferable
* @param nativeEvent
* @param handleDragEvents
* if true, {@link VDragAndDropManager} handles the drag and drop
* operation GWT event preview.
* @return
*/
public VDragEvent startDrag(VTransferable transferable,
final NativeEvent startEvent, final boolean handleDragEvents) {
interruptDrag();
isStarted = false;
currentDrag = new VDragEvent(transferable, startEvent);
currentDrag.setCurrentGwtEvent(startEvent);
final Command startDrag = new Command() {
public void execute() {
isStarted = true;
VDropHandler dh = null;
if (startEvent != null) {
dh = findDragTarget(Element.as(currentDrag
.getCurrentGwtEvent().getEventTarget()));
}
if (dh != null) {
// drag has started on a DropHandler, kind of drag over
// happens
currentDropHandler = dh;
dh.dragEnter(currentDrag);
}
if (handleDragEvents) {
handlerRegistration = Event
.addNativePreviewHandler(defaultDragAndDropEventHandler);
if (dragElement != null
&& dragElement.getParentElement() == null) {
// deferred attaching drag image is on going, we can
// hurry with it now
lazyAttachDragElement.cancel();
lazyAttachDragElement.run();
}
}
// just capture something to prevent text selection in IE
Event.setCapture(RootPanel.getBodyElement());
}
};
if (handleDragEvents
&& Event.as(startEvent).getTypeInt() == Event.ONMOUSEDOWN) {
// only really start drag event on mousemove
deferredStartRegistration = Event
.addNativePreviewHandler(new NativePreviewHandler() {
public void onPreviewNativeEvent(
NativePreviewEvent event) {
int typeInt = event.getTypeInt();
switch (typeInt) {
case Event.ONMOUSEOVER:
if (dragElement == null) {
break;
}
EventTarget currentEventTarget = event
.getNativeEvent()
.getCurrentEventTarget();
if (Node.is(currentEventTarget)
&& !dragElement.isOrHasChild(Node
.as(currentEventTarget))) {
// drag image appeared below, ignore
break;
}
case Event.ONKEYDOWN:
case Event.ONKEYPRESS:
case Event.ONKEYUP:
case Event.ONBLUR:
// don't cancel possible drag start
break;
case Event.ONMOUSEOUT:
if (dragElement == null) {
break;
}
EventTarget relatedEventTarget = event
.getNativeEvent()
.getRelatedEventTarget();
if (Node.is(relatedEventTarget)
&& !dragElement.isOrHasChild(Node
.as(relatedEventTarget))) {
// drag image appeared below, ignore
break;
}
case Event.ONMOUSEMOVE:
deferredStartRegistration.removeHandler();
deferredStartRegistration = null;
currentDrag.setCurrentGwtEvent(event
.getNativeEvent());
startDrag.execute();
break;
default:
// on any other events, clean up the
// deferred drag start
deferredStartRegistration.removeHandler();
deferredStartRegistration = null;
currentDrag = null;
clearDragElement();
break;
}
}
});
} else {
startDrag.execute();
}
return currentDrag;
}
private void updateDragImagePosition() {
if (currentDrag.getCurrentGwtEvent() != null && dragElement != null) {
Style style = dragElement.getStyle();
int clientY = currentDrag.getCurrentGwtEvent().getClientY();
int clientX = currentDrag.getCurrentGwtEvent().getClientX();
style.setTop(clientY, Unit.PX);
style.setLeft(clientX, Unit.PX);
}
}
/**
* First seeks the widget from this element, then iterates widgets until one
* implement HasDropHandler. Returns DropHandler from that.
*
* @param element
* @return
*/
private VDropHandler findDragTarget(Element element) {
try {
Widget w = Util.findWidget(
(com.google.gwt.user.client.Element) element, null);
if (w == null) {
return null;
}
while (!(w instanceof VHasDropHandler)) {
w = w.getParent();
if (w == null) {
break;
}
}
if (w == null) {
return null;
} else {
VDropHandler dh = ((VHasDropHandler) w).getDropHandler();
return dh;
}
} catch (Exception e) {
// ApplicationConnection.getConsole().log(
// "FIXME: Exception when detecting drop handler");
// e.printStackTrace();
return null;
}
}
/**
* Drag is ended (drop happened) on current drop handler. Calls drop method
* on current drop handler and does appropriate cleanup.
*/
public void endDrag() {
endDrag(true);
}
/**
* The drag and drop operation is ended, but drop did not happen. If
* operation is currently on a drop handler, its dragLeave method is called
* and appropriate cleanup happens.
*/
public void interruptDrag() {
endDrag(false);
}
private void endDrag(boolean doDrop) {
if (handlerRegistration != null) {
handlerRegistration.removeHandler();
handlerRegistration = null;
}
if (currentDropHandler != null) {
if (doDrop) {
// we have dropped on a drop target
boolean sendTransferableToServer = currentDropHandler
.drop(currentDrag);
if (sendTransferableToServer) {
doRequest(DragEventType.DROP);
}
} else {
currentDrag.setCurrentGwtEvent(null);
currentDropHandler.dragLeave(currentDrag);
}
currentDropHandler = null;
serverCallback = null;
visitId = 0; // reset to ignore ongoing server check
}
currentDrag = null;
clearDragElement();
// release the capture (set to prevent text selection in IE)
Event.releaseCapture(RootPanel.getBodyElement());
}
private void clearDragElement() {
if (dragElement != null) {
if (dragElement.getParentElement() != null) {
RootPanel.getBodyElement().removeChild(dragElement);
}
dragElement = null;
}
}
private int visitId = 0;
private Element dragElement;
/**
* Visits server during drag and drop procedure. Transferable and event type
* is given to server side counterpart of DropHandler.
*
* If another server visit is started before the current is received, the
* current is just dropped. TODO consider if callback should have
* interrupted() method for cleanup.
*
* @param acceptCallback
*/
public void visitServer(VDragEventServerCallback acceptCallback) {
doRequest(DragEventType.ENTER);
serverCallback = acceptCallback;
}
private void doRequest(DragEventType drop) {
if (currentDropHandler == null) {
return;
}
Paintable paintable = currentDropHandler.getPaintable();
ApplicationConnection client = currentDropHandler
.getApplicationConnection();
/*
* For drag events we are using special id that are routed to
* "drag service" which then again finds the corresponding DropHandler
* on server side.
*
* TODO add rest of the data in Transferable
*
* TODO implement partial updates to Transferable (currently the whole
* Transferable is sent on each request)
*/
visitId++;
client.updateVariable(DD_SERVICE, "visitId", visitId, false);
client.updateVariable(DD_SERVICE, "eventId", currentDrag.getEventId(),
false);
client.updateVariable(DD_SERVICE, "dhowner", paintable, false);
VTransferable transferable = currentDrag.getTransferable();
client.updateVariable(DD_SERVICE, "component",
transferable.getDragSource(), false);
client.updateVariable(DD_SERVICE, "type", drop.ordinal(), false);
if (currentDrag.getCurrentGwtEvent() != null) {
try {
MouseEventDetails mouseEventDetails = new MouseEventDetails(
currentDrag.getCurrentGwtEvent());
currentDrag.getDropDetails().put("mouseEvent",
mouseEventDetails.serialize());
} catch (Exception e) {
// NOP, (at least oophm on Safari) can't serialize html dd event
// to mouseevent
}
} else {
currentDrag.getDropDetails().put("mouseEvent", null);
}
client.updateVariable(DD_SERVICE, "evt", currentDrag.getDropDetails(),
false);
client.updateVariable(DD_SERVICE, "tra", transferable.getVariableMap(),
true);
}
public void handleServerResponse(ValueMap valueMap) {
if (serverCallback == null) {
return;
}
UIDL uidl = (UIDL) valueMap.cast();
int visitId = uidl.getIntAttribute("visitId");
if (this.visitId == visitId) {
serverCallback.handleResponse(uidl.getBooleanAttribute("accepted"),
uidl);
serverCallback = null;
}
runDeferredCommands();
}
private void runDeferredCommands() {
if (deferredCommand != null) {
Command command = deferredCommand;
deferredCommand = null;
command.execute();
if (!isBusy()) {
runDeferredCommands();
}
}
}
void setDragElement(Element node) {
if (currentDrag != null) {
if (dragElement != null && dragElement != node) {
clearDragElement();
} else if (node == dragElement) {
return;
}
dragElement = node;
dragElement.addClassName("v-drag-element");
updateDragImagePosition();
if (isStarted) {
lazyAttachDragElement.run();
} else {
/*
* To make our default dnd handler as compatible as possible, we
* need to defer the appearance of dragElement. Otherwise events
* that are derived from sequences of other events might not
* fire as domchanged will fire between them or mouse up might
* happen on dragElement.
*/
lazyAttachDragElement.schedule(300);
}
}
}
Element getDragElement() {
return dragElement;
}
private final Timer lazyAttachDragElement = new Timer() {
@Override
public void run() {
if (dragElement != null && dragElement.getParentElement() == null) {
RootPanel.getBodyElement().appendChild(dragElement);
}
}
};
private Command deferredCommand;
private boolean isBusy() {
return serverCallback != null;
}
/**
* Method to que tasks until all dd related server visits are done
*
* @param command
*/
private void defer(Command command) {
deferredCommand = command;
}
/**
* Method to execute commands when all existing dd related tasks are
* completed (some may require server visit).
* <p>
* Using this method may be handy if criterion that uses lazy initialization
* are used. Check
* <p>
* TODO Optimization: consider if we actually only need to keep the last
* command in queue here.
*
* @param command
*/
public void executeWhenReady(Command command) {
if (isBusy()) {
defer(command);
} else {
command.execute();
}
}
}