You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

VDragAndDropManager.java 25KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. /*
  2. @ITMillApache2LicenseForJavaFiles@
  3. */
  4. package com.vaadin.terminal.gwt.client.ui.dd;
  5. import com.google.gwt.dom.client.Element;
  6. import com.google.gwt.dom.client.EventTarget;
  7. import com.google.gwt.dom.client.NativeEvent;
  8. import com.google.gwt.dom.client.Node;
  9. import com.google.gwt.dom.client.Style;
  10. import com.google.gwt.dom.client.Style.Display;
  11. import com.google.gwt.dom.client.Style.Unit;
  12. import com.google.gwt.event.dom.client.KeyCodes;
  13. import com.google.gwt.event.shared.HandlerRegistration;
  14. import com.google.gwt.user.client.Command;
  15. import com.google.gwt.user.client.Event;
  16. import com.google.gwt.user.client.Event.NativePreviewEvent;
  17. import com.google.gwt.user.client.Event.NativePreviewHandler;
  18. import com.google.gwt.user.client.Timer;
  19. import com.google.gwt.user.client.ui.RootPanel;
  20. import com.google.gwt.user.client.ui.Widget;
  21. import com.vaadin.terminal.gwt.client.ApplicationConnection;
  22. import com.vaadin.terminal.gwt.client.MouseEventDetails;
  23. import com.vaadin.terminal.gwt.client.Paintable;
  24. import com.vaadin.terminal.gwt.client.UIDL;
  25. import com.vaadin.terminal.gwt.client.Util;
  26. import com.vaadin.terminal.gwt.client.ValueMap;
  27. /**
  28. * Helper class to manage the state of drag and drop event on Vaadin client
  29. * side. Can be used to implement most of the drag and drop operation
  30. * automatically via cross-browser event preview method or just as a helper when
  31. * implementing own low level drag and drop operation (like with HTML5 api).
  32. * <p>
  33. * Singleton. Only one drag and drop operation can be active anyways. Use
  34. * {@link #get()} to get instance.
  35. *
  36. */
  37. public class VDragAndDropManager {
  38. private final class DefaultDragAndDropEventHandler implements
  39. NativePreviewHandler {
  40. public void onPreviewNativeEvent(NativePreviewEvent event) {
  41. NativeEvent nativeEvent = event.getNativeEvent();
  42. int typeInt = event.getTypeInt();
  43. if (typeInt == Event.ONKEYDOWN) {
  44. int keyCode = event.getNativeEvent().getKeyCode();
  45. if (keyCode == KeyCodes.KEY_ESCAPE) {
  46. // end drag if ESC is hit
  47. interruptDrag();
  48. event.cancel();
  49. event.getNativeEvent().preventDefault();
  50. }
  51. // no use for handling for any key down event
  52. return;
  53. }
  54. currentDrag.setCurrentGwtEvent(nativeEvent);
  55. updateDragImagePosition();
  56. Element targetElement = Element.as(nativeEvent.getEventTarget());
  57. if (dragElement != null && dragElement.isOrHasChild(targetElement)) {
  58. // to detect the "real" target, hide dragelement temporary and
  59. // use elementFromPoint
  60. String display = dragElement.getStyle().getDisplay();
  61. dragElement.getStyle().setDisplay(Display.NONE);
  62. try {
  63. int x = nativeEvent.getClientX();
  64. int y = nativeEvent.getClientY();
  65. // Util.browserDebugger();
  66. targetElement = Util.getElementFromPoint(x, y);
  67. if (targetElement == null) {
  68. // ApplicationConnection.getConsole().log(
  69. // "Event on dragImage, ignored");
  70. event.cancel();
  71. nativeEvent.stopPropagation();
  72. return;
  73. } else {
  74. // ApplicationConnection.getConsole().log(
  75. // "Event on dragImage, target changed");
  76. // special handling for events over dragImage
  77. // pretty much all events are mousemove althout below
  78. // kind of happens mouseover
  79. switch (typeInt) {
  80. case Event.ONMOUSEOVER:
  81. case Event.ONMOUSEOUT:
  82. // ApplicationConnection
  83. // .getConsole()
  84. // .log(
  85. // "IGNORING proxy image event, fired because of hack or not significant");
  86. return;
  87. case Event.ONMOUSEMOVE:
  88. VDropHandler findDragTarget = findDragTarget(targetElement);
  89. if (findDragTarget != currentDropHandler) {
  90. // dragleave on old
  91. if (currentDropHandler != null) {
  92. currentDropHandler.dragLeave(currentDrag);
  93. currentDrag.getDropDetails().clear();
  94. serverCallback = null;
  95. }
  96. // dragenter on new
  97. currentDropHandler = findDragTarget;
  98. if (findDragTarget != null) {
  99. // ApplicationConnection.getConsole().log(
  100. // "DropHandler now"
  101. // + currentDropHandler
  102. // .getPaintable());
  103. }
  104. if (currentDropHandler != null) {
  105. currentDrag
  106. .setElementOver((com.google.gwt.user.client.Element) targetElement);
  107. currentDropHandler.dragEnter(currentDrag);
  108. }
  109. } else if (findDragTarget != null) {
  110. currentDrag
  111. .setElementOver((com.google.gwt.user.client.Element) targetElement);
  112. currentDropHandler.dragOver(currentDrag);
  113. }
  114. // prevent text selection on IE
  115. nativeEvent.preventDefault();
  116. return;
  117. default:
  118. // just update element over and let the actual
  119. // handling code do the thing
  120. // ApplicationConnection.getConsole().log(
  121. // "Target just modified on "
  122. // + event.getType());
  123. currentDrag
  124. .setElementOver((com.google.gwt.user.client.Element) targetElement);
  125. break;
  126. }
  127. }
  128. } catch (RuntimeException e) {
  129. // ApplicationConnection.getConsole().log(
  130. // "ERROR during elementFromPoint hack.");
  131. throw e;
  132. } finally {
  133. dragElement.getStyle().setProperty("display", display);
  134. }
  135. }
  136. switch (typeInt) {
  137. case Event.ONMOUSEOVER:
  138. VDropHandler target = findDragTarget(targetElement);
  139. if (target != null && target != currentDropHandler) {
  140. if (currentDropHandler != null) {
  141. currentDropHandler.dragLeave(currentDrag);
  142. currentDrag.getDropDetails().clear();
  143. }
  144. currentDropHandler = target;
  145. // ApplicationConnection.getConsole().log(
  146. // "DropHandler now"
  147. // + currentDropHandler.getPaintable());
  148. target.dragEnter(currentDrag);
  149. } else if (target == null && currentDropHandler != null) {
  150. // ApplicationConnection.getConsole().log("Invalid state!?");
  151. currentDropHandler.dragLeave(currentDrag);
  152. currentDrag.getDropDetails().clear();
  153. currentDropHandler = null;
  154. }
  155. break;
  156. case Event.ONMOUSEOUT:
  157. Element relatedTarget = Element.as(nativeEvent
  158. .getRelatedEventTarget());
  159. VDropHandler newDragHanler = findDragTarget(relatedTarget);
  160. if (dragElement != null
  161. && dragElement.isOrHasChild(relatedTarget)) {
  162. // ApplicationConnection.getConsole().log(
  163. // "Mouse out of dragImage, ignored");
  164. return;
  165. }
  166. if (currentDropHandler != null
  167. && currentDropHandler != newDragHanler) {
  168. currentDropHandler.dragLeave(currentDrag);
  169. currentDrag.getDropDetails().clear();
  170. currentDropHandler = null;
  171. serverCallback = null;
  172. }
  173. break;
  174. case Event.ONMOUSEMOVE:
  175. if (currentDropHandler != null) {
  176. currentDropHandler.dragOver(currentDrag);
  177. }
  178. nativeEvent.preventDefault();
  179. break;
  180. case Event.ONMOUSEUP:
  181. endDrag();
  182. break;
  183. default:
  184. break;
  185. }
  186. }
  187. }
  188. public enum DragEventType {
  189. ENTER, LEAVE, OVER, DROP
  190. }
  191. private static final String DD_SERVICE = "DD";
  192. private static VDragAndDropManager instance;
  193. private HandlerRegistration handlerRegistration;
  194. private VDragEvent currentDrag;
  195. /**
  196. * If dragging is currently on a drophandler, this field has reference to it
  197. */
  198. private VDropHandler currentDropHandler;
  199. public VDropHandler getCurrentDropHandler() {
  200. return currentDropHandler;
  201. }
  202. /**
  203. * If drag and drop operation is not handled by {@link VDragAndDropManager}s
  204. * internal handler, this can be used to update current {@link VDropHandler}
  205. * .
  206. *
  207. * @param currentDropHandler
  208. */
  209. public void setCurrentDropHandler(VDropHandler currentDropHandler) {
  210. this.currentDropHandler = currentDropHandler;
  211. }
  212. private VDragEventServerCallback serverCallback;
  213. private HandlerRegistration deferredStartRegistration;
  214. public static VDragAndDropManager get() {
  215. if (instance == null) {
  216. instance = new VDragAndDropManager();
  217. }
  218. return instance;
  219. }
  220. /* Singleton */
  221. private VDragAndDropManager() {
  222. }
  223. private NativePreviewHandler defaultDragAndDropEventHandler = new DefaultDragAndDropEventHandler();
  224. /**
  225. * Flag to indicate if drag operation has really started or not. Null check
  226. * of currentDrag field is not enough as a lazy start may be pending.
  227. */
  228. private boolean isStarted;
  229. /**
  230. * This method is used to start Vaadin client side drag and drop operation.
  231. * Operation may be started by virtually any Widget.
  232. * <p>
  233. * Cancels possible existing drag. TODO figure out if this is always a bug
  234. * if one is active. Maybe a good and cheap lifesaver thought.
  235. * <p>
  236. * If possible, method automatically detects current {@link VDropHandler}
  237. * and fires {@link VDropHandler#dragEnter(VDragEvent)} event on it.
  238. * <p>
  239. * May also be used to control the drag and drop operation. If this option
  240. * is used, {@link VDropHandler} is searched on mouse events and appropriate
  241. * methods on it called automatically.
  242. *
  243. * @param transferable
  244. * @param nativeEvent
  245. * @param handleDragEvents
  246. * if true, {@link VDragAndDropManager} handles the drag and drop
  247. * operation GWT event preview.
  248. * @return
  249. */
  250. public VDragEvent startDrag(VTransferable transferable,
  251. final NativeEvent startEvent, final boolean handleDragEvents) {
  252. interruptDrag();
  253. isStarted = false;
  254. currentDrag = new VDragEvent(transferable, startEvent);
  255. currentDrag.setCurrentGwtEvent(startEvent);
  256. final Command startDrag = new Command() {
  257. public void execute() {
  258. isStarted = true;
  259. VDropHandler dh = null;
  260. if (startEvent != null) {
  261. dh = findDragTarget(Element.as(currentDrag
  262. .getCurrentGwtEvent().getEventTarget()));
  263. }
  264. if (dh != null) {
  265. // drag has started on a DropHandler, kind of drag over
  266. // happens
  267. currentDropHandler = dh;
  268. dh.dragEnter(currentDrag);
  269. }
  270. if (handleDragEvents) {
  271. handlerRegistration = Event
  272. .addNativePreviewHandler(defaultDragAndDropEventHandler);
  273. if (dragElement != null
  274. && dragElement.getParentElement() == null) {
  275. // deferred attaching drag image is on going, we can
  276. // hurry with it now
  277. lazyAttachDragElement.cancel();
  278. lazyAttachDragElement.run();
  279. }
  280. }
  281. // just capture something to prevent text selection in IE
  282. Event.setCapture(RootPanel.getBodyElement());
  283. }
  284. };
  285. if (handleDragEvents
  286. && Event.as(startEvent).getTypeInt() == Event.ONMOUSEDOWN) {
  287. // only really start drag event on mousemove
  288. deferredStartRegistration = Event
  289. .addNativePreviewHandler(new NativePreviewHandler() {
  290. public void onPreviewNativeEvent(
  291. NativePreviewEvent event) {
  292. int typeInt = event.getTypeInt();
  293. switch (typeInt) {
  294. case Event.ONMOUSEOVER:
  295. if (dragElement == null) {
  296. break;
  297. }
  298. EventTarget currentEventTarget = event
  299. .getNativeEvent()
  300. .getCurrentEventTarget();
  301. if (Node.is(currentEventTarget)
  302. && !dragElement.isOrHasChild(Node
  303. .as(currentEventTarget))) {
  304. // drag image appeared below, ignore
  305. break;
  306. }
  307. case Event.ONKEYDOWN:
  308. case Event.ONKEYPRESS:
  309. case Event.ONKEYUP:
  310. case Event.ONBLUR:
  311. // don't cancel possible drag start
  312. break;
  313. case Event.ONMOUSEOUT:
  314. if (dragElement == null) {
  315. break;
  316. }
  317. EventTarget relatedEventTarget = event
  318. .getNativeEvent()
  319. .getRelatedEventTarget();
  320. if (Node.is(relatedEventTarget)
  321. && !dragElement.isOrHasChild(Node
  322. .as(relatedEventTarget))) {
  323. // drag image appeared below, ignore
  324. break;
  325. }
  326. case Event.ONMOUSEMOVE:
  327. deferredStartRegistration.removeHandler();
  328. deferredStartRegistration = null;
  329. currentDrag.setCurrentGwtEvent(event
  330. .getNativeEvent());
  331. startDrag.execute();
  332. break;
  333. default:
  334. // on any other events, clean up the
  335. // deferred drag start
  336. deferredStartRegistration.removeHandler();
  337. deferredStartRegistration = null;
  338. currentDrag = null;
  339. clearDragElement();
  340. break;
  341. }
  342. }
  343. });
  344. } else {
  345. startDrag.execute();
  346. }
  347. return currentDrag;
  348. }
  349. private void updateDragImagePosition() {
  350. if (currentDrag.getCurrentGwtEvent() != null && dragElement != null) {
  351. Style style = dragElement.getStyle();
  352. int clientY = currentDrag.getCurrentGwtEvent().getClientY();
  353. int clientX = currentDrag.getCurrentGwtEvent().getClientX();
  354. style.setTop(clientY, Unit.PX);
  355. style.setLeft(clientX, Unit.PX);
  356. }
  357. }
  358. /**
  359. * First seeks the widget from this element, then iterates widgets until one
  360. * implement HasDropHandler. Returns DropHandler from that.
  361. *
  362. * @param element
  363. * @return
  364. */
  365. private VDropHandler findDragTarget(Element element) {
  366. try {
  367. Widget w = Util.findWidget(
  368. (com.google.gwt.user.client.Element) element, null);
  369. if (w == null) {
  370. return null;
  371. }
  372. while (!(w instanceof VHasDropHandler)) {
  373. w = w.getParent();
  374. if (w == null) {
  375. break;
  376. }
  377. }
  378. if (w == null) {
  379. return null;
  380. } else {
  381. VDropHandler dh = ((VHasDropHandler) w).getDropHandler();
  382. return dh;
  383. }
  384. } catch (Exception e) {
  385. // ApplicationConnection.getConsole().log(
  386. // "FIXME: Exception when detecting drop handler");
  387. // e.printStackTrace();
  388. return null;
  389. }
  390. }
  391. /**
  392. * Drag is ended (drop happened) on current drop handler. Calls drop method
  393. * on current drop handler and does appropriate cleanup.
  394. */
  395. public void endDrag() {
  396. endDrag(true);
  397. }
  398. /**
  399. * The drag and drop operation is ended, but drop did not happen. If
  400. * operation is currently on a drop handler, its dragLeave method is called
  401. * and appropriate cleanup happens.
  402. */
  403. public void interruptDrag() {
  404. endDrag(false);
  405. }
  406. private void endDrag(boolean doDrop) {
  407. if (handlerRegistration != null) {
  408. handlerRegistration.removeHandler();
  409. handlerRegistration = null;
  410. }
  411. if (currentDropHandler != null) {
  412. if (doDrop) {
  413. // we have dropped on a drop target
  414. boolean sendTransferableToServer = currentDropHandler
  415. .drop(currentDrag);
  416. if (sendTransferableToServer) {
  417. doRequest(DragEventType.DROP);
  418. }
  419. } else {
  420. currentDrag.setCurrentGwtEvent(null);
  421. currentDropHandler.dragLeave(currentDrag);
  422. }
  423. currentDropHandler = null;
  424. serverCallback = null;
  425. visitId = 0; // reset to ignore ongoing server check
  426. }
  427. currentDrag = null;
  428. clearDragElement();
  429. // release the capture (set to prevent text selection in IE)
  430. Event.releaseCapture(RootPanel.getBodyElement());
  431. }
  432. private void clearDragElement() {
  433. if (dragElement != null) {
  434. if (dragElement.getParentElement() != null) {
  435. RootPanel.getBodyElement().removeChild(dragElement);
  436. }
  437. dragElement = null;
  438. }
  439. }
  440. private int visitId = 0;
  441. private Element dragElement;
  442. /**
  443. * Visits server during drag and drop procedure. Transferable and event type
  444. * is given to server side counterpart of DropHandler.
  445. *
  446. * If another server visit is started before the current is received, the
  447. * current is just dropped. TODO consider if callback should have
  448. * interrupted() method for cleanup.
  449. *
  450. * @param acceptCallback
  451. */
  452. public void visitServer(VDragEventServerCallback acceptCallback) {
  453. doRequest(DragEventType.ENTER);
  454. serverCallback = acceptCallback;
  455. }
  456. private void doRequest(DragEventType drop) {
  457. if (currentDropHandler == null) {
  458. return;
  459. }
  460. Paintable paintable = currentDropHandler.getPaintable();
  461. ApplicationConnection client = currentDropHandler
  462. .getApplicationConnection();
  463. /*
  464. * For drag events we are using special id that are routed to
  465. * "drag service" which then again finds the corresponding DropHandler
  466. * on server side.
  467. *
  468. * TODO add rest of the data in Transferable
  469. *
  470. * TODO implement partial updates to Transferable (currently the whole
  471. * Transferable is sent on each request)
  472. */
  473. visitId++;
  474. client.updateVariable(DD_SERVICE, "visitId", visitId, false);
  475. client.updateVariable(DD_SERVICE, "eventId", currentDrag.getEventId(),
  476. false);
  477. client.updateVariable(DD_SERVICE, "dhowner", paintable, false);
  478. VTransferable transferable = currentDrag.getTransferable();
  479. client.updateVariable(DD_SERVICE, "component",
  480. transferable.getDragSource(), false);
  481. client.updateVariable(DD_SERVICE, "type", drop.ordinal(), false);
  482. if (currentDrag.getCurrentGwtEvent() != null) {
  483. try {
  484. MouseEventDetails mouseEventDetails = new MouseEventDetails(
  485. currentDrag.getCurrentGwtEvent());
  486. currentDrag.getDropDetails().put("mouseEvent",
  487. mouseEventDetails.serialize());
  488. } catch (Exception e) {
  489. // NOP, (at least oophm on Safari) can't serialize html dd event
  490. // to mouseevent
  491. }
  492. } else {
  493. currentDrag.getDropDetails().put("mouseEvent", null);
  494. }
  495. client.updateVariable(DD_SERVICE, "evt", currentDrag.getDropDetails(),
  496. false);
  497. client.updateVariable(DD_SERVICE, "tra", transferable.getVariableMap(),
  498. true);
  499. }
  500. public void handleServerResponse(ValueMap valueMap) {
  501. if (serverCallback == null) {
  502. return;
  503. }
  504. UIDL uidl = (UIDL) valueMap.cast();
  505. int visitId = uidl.getIntAttribute("visitId");
  506. if (this.visitId == visitId) {
  507. serverCallback.handleResponse(uidl.getBooleanAttribute("accepted"),
  508. uidl);
  509. serverCallback = null;
  510. }
  511. runDeferredCommands();
  512. }
  513. private void runDeferredCommands() {
  514. if (deferredCommand != null) {
  515. Command command = deferredCommand;
  516. deferredCommand = null;
  517. command.execute();
  518. if (!isBusy()) {
  519. runDeferredCommands();
  520. }
  521. }
  522. }
  523. void setDragElement(Element node) {
  524. if (currentDrag != null) {
  525. if (dragElement != null && dragElement != node) {
  526. clearDragElement();
  527. } else if (node == dragElement) {
  528. return;
  529. }
  530. dragElement = node;
  531. dragElement.addClassName("v-drag-element");
  532. updateDragImagePosition();
  533. if (isStarted) {
  534. lazyAttachDragElement.run();
  535. } else {
  536. /*
  537. * To make our default dnd handler as compatible as possible, we
  538. * need to defer the appearance of dragElement. Otherwise events
  539. * that are derived from sequences of other events might not
  540. * fire as domchanged will fire between them or mouse up might
  541. * happen on dragElement.
  542. */
  543. lazyAttachDragElement.schedule(300);
  544. }
  545. }
  546. }
  547. Element getDragElement() {
  548. return dragElement;
  549. }
  550. private final Timer lazyAttachDragElement = new Timer() {
  551. @Override
  552. public void run() {
  553. if (dragElement != null && dragElement.getParentElement() == null) {
  554. RootPanel.getBodyElement().appendChild(dragElement);
  555. }
  556. }
  557. };
  558. private Command deferredCommand;
  559. private boolean isBusy() {
  560. return serverCallback != null;
  561. }
  562. /**
  563. * Method to que tasks until all dd related server visits are done
  564. *
  565. * @param command
  566. */
  567. private void defer(Command command) {
  568. deferredCommand = command;
  569. }
  570. /**
  571. * Method to execute commands when all existing dd related tasks are
  572. * completed (some may require server visit).
  573. * <p>
  574. * Using this method may be handy if criterion that uses lazy initialization
  575. * are used. Check
  576. * <p>
  577. * TODO Optimization: consider if we actually only need to keep the last
  578. * command in queue here.
  579. *
  580. * @param command
  581. */
  582. public void executeWhenReady(Command command) {
  583. if (isBusy()) {
  584. defer(command);
  585. } else {
  586. command.execute();
  587. }
  588. }
  589. }