- Stuff You're Going to Learn
- The Drag-and-Drop Example Application
- The Drag-and-Drop Module
- Implementation of the Drag-and-Drop Application
- Drag and Drop Implementation in a GWT Module
- Stuff We Covered in This Solution
Drag and Drop Implementation in a GWT Module
Now that we have a good grasp of how to use the dnd module, let's look at how it's implemented.
The drag-and-drop module is implemented inside, our Components module. Figure 6.5 shows the drag and drop's pertinent files and directories.
Figure 6.5 Drag-and-drop module's files and directories
Like all GWT modules, our drag-and-drop module has an XML configuration file. Like most modules, our drag-and-drop module also has some Java classes and interfaces.
The Module Configuration File
Every GWT module must provide a configuration file. The dnd module's configuration file is listed in Listing 6.10.
Listing 6.10. com/gwtsolutions/dnd/Dnd.gwt.xml
1.<module> 2. <inherits name='com.google.gwt.core.Core'/> 3.</module>
It doesn't get any simpler than that. All we need for our dnd module is the core GWT classes, so that's what we inherit.
Now let's look at the Java classes in the dnd module.
The Abstract Drag Source and Drop Target Classes
The DragSource class is listed in Listing 6.11.
Listing 6.11. com.gwtsolutions.components.client.ui.dnd.DragSource
1.package com.gwtsolutions.components.client.ui.dnd; 2. 3.import com.google.gwt.user.client.ui.AbsolutePanel; 4.import com.google.gwt.user.client.ui.MouseListener; 5.import com.google.gwt.user.client.ui.Widget; 6.import com.gwtsolutions.components.client.ui.MousePanel; 7.import com.gwtsolutions.components.client.ui.Point; 8. 9.public abstract class DragSource extends MousePanel { 10. private static final String BAD_PARENT = 11. "Drag sources must have a parent of type AbsolutePanel"; 12. private static final MouseListener defaultDragger = 13. new FollowsMouseDragger(); 14. 15. private boolean dragging = false; 16. private Point originalLocation = null; 17. private DropTarget enclosingDropTarget = null; 18. 19. public DragSource(Widget w) { 20. // Drag sources contain only one widget, which is 21. // the widget passed to this constructor 22. add(w); 23. 24. // Listener order is significant. See the text 25. // of GWT Solutions for more information 26. addMouseListener(new DragSourceListener()); 27. addMouseListener(getMouseListener()); 28. } 29. 30. public void onLoad() { 31. // GWT calls this method when the drag source's 32. // DOM element is added to the browser's DOM tree. 33. if ( ! (getParent() instanceof AbsolutePanel)) 34. throw new IllegalStateException(BAD_PARENT); 35. } 36. 37. public void dragStarted() { 38. // subclasses can override this no-op method 39. // as needed 40. } 41. 42. public void droppedOutsideDropTarget() { 43. // By default, when a drag source is dropped outside 44. // of any drop target, it is returned to its original 45. // position. Subclasses can override this method to 46. // change or augment that behavior 47. returnToOriginalPosition(); 48. } 49. 50. public void acceptedByDropTarget(DropTarget dt) { 51. // subclasses can override this no-op method 52. // as needed 53. } 54. 55. public void rejectedByDropTarget(DropTarget dt) { 56. // By default, when a drag source is rejected by 57. // a drop target, it is returned to its original 58. // position. Subclasses can override this method to 59. // change or augment that behavior 60. returnToOriginalPosition(); 61. } 62. 63. public boolean isDragging() { 64. return dragging; 65. } 66. 67. public void setDragging(boolean dragging) { 68. this.dragging = dragging; 69. } 70. 71. public void setOriginalLocation(Point originalLocation) { 72. this.originalLocation = originalLocation; 73. } 74. 75. public DropTarget getEnclosingDropTarget() { 76. return enclosingDropTarget; 77. } 78. 79. public void setEnclosingDropTarget( 80. DropTarget enclosingDropTarget) { 81. this.enclosingDropTarget = enclosingDropTarget; 82. } 83. 84. protected void returnToOriginalPosition() { 85. AbsolutePanel ap = (AbsolutePanel) getParent(); 86. ap.setWidgetPosition(this, originalLocation.x, 87. originalLocation.y); 88. } 89. 90. protected MouseListener getMouseListener() { 91. return defaultDragger; 92. } 93.}
This simple extension of the MousePanel we discussed in "The Viewport's Use of a Focus Panel: Revisited" (page 115) defines three properties and implements four methods that subclasses are likely to use: dragStarted(), droppedOutsideDropTarget(), acceptedByDropTarget(), and rejectedByDropTarget().
The properties keep track of whether the mouse panel is currently being dragged, its position before the drag began, and the enclosing drop target, if any. The methods are typically overridden by subclasses, as is the case for the MusicPlayerPanelDropTarget, listed in Listing 6.6 on page 178.
You may wonder why DragSource extends MousePanel. Here's why: Not all GWT widgets support mouse listeners; in fact, most do not, and we want to be able to drag any GWT component. So we wrap widgets in a mouse panel, which does support mouse listeners. Unbeknownst to users of the dnd module, they are really dragging mouse panels, which contain a single widget. We used this same technique in The Viewport's Use of a Focus Panel: Revisited" (page 115). See that section for more information about mouse panels and mouse listeners.
The DragSource class adds two mouse listeners to the widget that it wraps. The first listener, an instance of DragSourceListener, which is listed in Listing 6.15 on page 192, monitors the drag and invokes the abstract methods defined by the DragSource and DropTarget classes at the appropriate times.
The second listener, by default, is an instance of FollowsMouseDragger, which is listed in Listing 6.14 on page 191. That implementation of the MouseListener interface drags the drag source wherever the mouse goes. Notice that the mouse listener—an instance of FollowsMouseListener—is pluggable; DragSource subclasses can override getMouseListener() to provide a different dragger.
Oh, one more thing: The order in which we add listeners is significant because that is the order in which GWT will invoke them.2 For the drag-and-drop module to function properly, the drag source listener must be added first because the DragSourceListener's onMouseUp method turns into a no-op if the drag source is not being dragged (we don't want the drag source listener to react to mouse up events if the drag source is not being dragged). Because AbstractMouseDragger.onMouseUp() sets the drag source's dragged property to false, that method must be called after the DragSourceListener.onMouseUp(). If you reverse the order of the addition of the mouse listeners, you will see that the drag-and-drop module never reacts to mouse up events.
The DropTarget class is listed in Listing 6.12.
Listing 6.12. com.gwtsolutions.components.client.ui.dnd.DropTarget
1.package com.gwtsolutions.components.client.ui.dnd; 2. 3.import com.google.gwt.user.client.ui.Widget; 4.import com.gwtsolutions.components.client.ui.MousePanel; 5. 6.public abstract class DropTarget extends MousePanel { 7. public abstract boolean acceptsDragSource(DragSource ds); 8. 9. public DropTarget(Widget w) { 10. // This panel conatians only one widget, which is the 11. // widget passed to this constructor 12. add(w); 13. } 14. 15. public void dragSourceEntered(DragSource ds) { 16. // subclasses can override this no-op method 17. // as needed 18. } 19. 20. public void dragSourceExited(DragSource ds) { 21. // subclasses can override this no-op method 22. // as needed 23. } 24. 25. public void dragSourceDropped(DragSource ds) { 26. // If the drag source dropped on this drop target 27. // is acceptable, notify the drag source that it's been 28. // dropped on this drop target; otherwise, notify the 29. // drag source that it was rejected by this drop target 30. if (acceptsDragSource(ds)) 31. ds.acceptedByDropTarget(this); 32. else 33. ds.rejectedByDropTarget(this); 34. } 35.}
This is another extension of MousePanel because we want any GWT widget to be able to function as a drop target. This class provides no-op defaults for two of the three methods that subclasses are likely to override: dragSourceEntered() and dragSourceExited().
For dragSourceDropped(), if the drag source is acceptable to the drop target—indicated by the return value of acceptsDragSource(), which is an abstract method subclasses must implement—we tell the drag source that it was accepted by the drop target; otherwise, we notify the drag source that, sadly enough, it was rejected by the drop target.
Mouse Listeners
The final pieces of the dnd puzzle are the mouse listeners, where most of the complexity lies. Listing 6.13 lists the AbstractMouseDragger class, which blithely drags widgets around on an absolute panel.
Listing 6.13. com.gwtsolutions.components.client.ui.dnd.AbstractMouseDragger
1.package com.gwtsolutions.components.client.ui.dnd; 2. 3.import com.google.gwt.user.client.DOM; 4.import com.google.gwt.user.client.ui.AbsolutePanel; 5.import com.google.gwt.user.client.ui.MouseListenerAdapter; 6. 7.public abstract class AbstractMouseDragger extends 8. MouseListenerAdapter { 9. private int xoffset, yoffset; 10. 11. // Subclasses implement this method to override the 12. // proposed left edge of the dragSource after a drag 13. protected abstract int getNextLeft(int proposedLeft, 14. DragSource ds); 15. 16. // Subclasses implement this method to override the 17. // proposed top edge of the dragSource after a drag 18. protected abstract int getNextTop(int proposedTop, 19. DragSource ds); 20. 21. public void onMouseDown(DragSource ds, int x, int y) { 22. xoffset = x; 23. yoffset = y; 24. 25. // Enable event capturing, so that subsequent mouse 26. // events are all sent directly to the ds's 27. // DOM element 28. DOM.setCapture(ds.getElement()); 29. 30. // Tell the drag source that dragging has begun 31. ds.setDragging(true); 32. } 33. 34. public void onMouseMove(DragSource ds, int x, int y) { 35. if (ds.isDragging()) { 36. // If the drag source is being dragged, calculate 37. // the proposed left and top, and give subclasses 38. // a chance to adjust those values 39. AbsolutePanel ap = (AbsolutePanel) ds.getParent(); 40. int proposedLeft = x + ap.getWidgetLeft(ds) - xoffset; 41. int proposedRight = y + ap.getWidgetTop(ds) - yoffset; 42. 43. int nextLeft = getNextLeft(proposedLeft, ds); 44. int nextRight = getNextTop(proposedRight, ds); 45. 46. // Set the drag source's position to the next 47. // left and next right 48. ap.setWidgetPosition(ds, nextLeft, nextRight); 49. } 50. } 51. 52. public void onMouseUp(DragSource ds, int x, int y) { 53. // Tell the drag source that dragging is done and 54. // release the capture of mouse events that was set 55. // in onMouseDown() 56. ds.setDragging(false); 57. DOM.releaseCapture(ds.getElement()); 58. } 59. 60. protected int checkLeftBounds(int proposedLeft, 61. DragSource dragSource) { 62. // Adjust the left edge of the dragSource if it's outside 63. // the bounds of it's parent panel 64. AbsolutePanel panel = 65. (AbsolutePanel) dragSource.getParent(); 66. int dragSourceWidth = dragSource.getOffsetWidth(); 67. int panelWidth = panel.getOffsetWidth(); 68. int nextLeft = proposedLeft; 69. 70. if (proposedLeft + dragSourceWidth > panelWidth) 71. nextLeft = panelWidth - dragSourceWidth; 72. 73. nextLeft = nextLeft < 0 ? 0 : nextLeft; 74. return nextLeft; 75. } 76. 77. protected int checkTopBounds( 78. // Adjust the top edge of the dragSource if it's outside 79. // the bounds of it's parent panel 80. int proposedTop, DragSource dragSource) { 81. AbsolutePanel panel = 82. (AbsolutePanel) dragSource.getParent(); 83. int dragSourceHeight = dragSource.getOffsetHeight(); 84. int panelHeight = panel.getOffsetHeight(); 85. int nextRight = proposedTop; 86. 87. if (proposedTop + dragSourceHeight > panelHeight) 88. nextRight = panelHeight - dragSourceHeight; 89. 90. nextRight = nextRight < 0 ? 0 : nextRight; 91. return nextRight; 92. } 93.}
This class knows nothing about drag sources or drop targets; all it does is drag widgets. Most of the logic consists of basic math that calculates the next position of a widget and checks boundaries to make sure the widget does not escape its enclosing absolute panel.
The interesting parts of the class are the calls to DOM.setCapture() and DOM.releaseCapture(), in onMouseDown() and onMouseUp(), respectively. DOM.setCapture() captures all mouse events and makes them available only to the widget that it is passed until DOM.releaseCapture() is invoked, returning event handling to normal. That provides a significant boost to performance while a widget is being dragged, which gives us time to make sophisticated calculations, like those in the DragSourceListener class, listed in Listing 6.15.
One other interesting thing about the AbstractMouseDragger class: It's abstract because it defines two abstract methods that can be implemented by subclasses to plug in a different dragging algorithm. Those methods—getNextLeft() and getNextTop()—are passed proposed locations that follow the mouse and return final locations for the current mouse movement. Those methods can be implemented by subclasses for specialized dragging, such as dragging widgets only in the horizontal or vertical directions. One of those subclasses is the FollowsMouseDragger class, listed in Listing 6.14, which follows the mouse but restricts the widget being dragged to the bounds of its enclosing absolute panel by invoking the inherited methods checkLeftBounds() and checkTopBounds().
Listing 6.14. com.gwtsolutions.components.client.ui.dnd.FollowsMouseDragger
1.package com.gwtsolutions.components.client.ui.dnd; 2. 3.// This extension of AbstractMouseDragger drags a drag source 4.// so that it follows the mouse. 5.public class FollowsMouseDragger extends AbstractMouseDragger { 6. protected int getNextLeft(int proposedLeft, 7. DragSource dragSource) { 8. // Adjust left edge if the left edge is outside the 9. // bounds of the drag source's parent panel 10. return checkLeftBounds(proposedLeft, dragSource); 11. } 12. 13. protected int getNextTop(int proposedTop, 14. DragSource dragSource) { 15. // Adjust left edge if the top edge is outside the 16. // bounds of the drag source's parent panel 17. return checkTopBounds(proposedTop, dragSource); 18. } 19.}
The DragSourceListener class, which makes callbacks to drag sources and drop targets as a widget is dragged, is listed in Listing 6.15.
Listing 6.15. com.gwtsolutions.components.client.ui.dnd.DragSourceListener
1. package com.gwtsolutions.components.client.ui.dnd; 2. 3. import com.google.gwt.user.client.DOM; 4. import com.google.gwt.user.client.Event; 5. import com.google.gwt.user.client.EventPreview; 6. import com.google.gwt.user.client.ui.AbsolutePanel; 7. import com.google.gwt.user.client.ui.MouseListenerAdapter; 8. import com.google.gwt.user.client.ui.Widget; 9. import com.google.gwt.user.client.ui.WidgetCollection; 10.import com.gwtsolutions.components.client.ui.Point; 11. 12.import java.util.Iterator; 13. 14.public class DragSourceListener extends MouseListenerAdapter { 15. private final Point[] dsCorners = new Point[4]; 16. private final WidgetCollection dropTargets = 17. new WidgetCollection(null); 18. 19. // The following event preview prevents the browser 20. // from reacting to mouse drags as the user drags 21. // drag sources 22. private static EventPreview preventDefaultMouseEvents = 23. new EventPreview() { 24. public boolean onEventPreview(Event event) { 25. switch (DOM.eventGetType(event)) { 26. case Event.ONMOUSEDOWN: 27. case Event.ONMOUSEMOVE: 28. DOM.eventPreventDefault(event); 29. } 30. return true; 31. } 32. }; 33. 34. public void onMouseEnter(Widget sender) { 35. // Prevent the browser from reacting to mouse 36. // events once the cursor enters the drag source 37. DOM.addEventPreview(preventDefaultMouseEvents); 38. } 39. public void onMouseLeave(Widget sender) { 40. // Restore browser event handling when the cursor 41. // leaves the drag source 42. DOM.removeEventPreview(preventDefaultMouseEvents); 43. } 44. public void onMouseDown(Widget sender, int x, int y) { 45. // All drag sources must have an AbsolutePanel for a 46. // parent. This restriction is enforced in the 47. // drag source's onLoad method 48. AbsolutePanel parent = (AbsolutePanel)sender.getParent(); 49. Iterator widgetIterator = parent.iterator(); 50. 51. // Iterate over the parent's widgets and put all 52. // drop targets in the dropTargets widget collection 53. // for future reference (see intersectsDropTarget(), 54. // implemented below) 55. while (widgetIterator.hasNext()) { 56. Widget w = (Widget) widgetIterator.next(); 57. if (w instanceof DropTarget) { 58. dropTargets.add(w); 59. } 60. } 61. 62. // Set the original location of the drag source in 63. // case the drag source is dropped outside any drop 64. // targets or is dropped on a drop target that rejects 65. // the drag source 66. DragSource ds = (DragSource) sender; 67. ds.setOriginalLocation(new Point(parent.getWidgetLeft(ds), 68. parent.getWidgetTop(ds))); 69. 70. // Notify the drag source that a drag has been 71. // initiated 72. ds.dragStarted(); 73. } 74. 75. public void onMouseMove(Widget sender, int x, int y) { 76. DragSource ds = (DragSource) sender; 77. if (!ds.isDragging()) { 78. // Don't do anything if the drag source is 79. // not being dragged 80. return; 81. } 82. 83. Widget dsWidget = ds.getWidget(); 84. DropTarget dt = intersectsDropTarget(dsWidget); 85. 86. // If the drag source intersects a drop target... 87. if (dt != null) { 88. // ...and if the drag source just entered 89. // the drop target... 90. if (ds.getEnclosingDropTarget() == null) { 91. // ...set the enclosing drop target and 92. // notify the drop target that the drag source 93. // has entered 94. ds.setEnclosingDropTarget(dt); 95. dt.dragSourceEntered(ds); 96. } 97. } 98. // If the drag source is not intersecting a drop 99. // target... 100. else { 101. DropTarget enclosingDropTarget = 102. ds.getEnclosingDropTarget(); 103. 104. // ...and the drag source was inside a drop target 105. // previously... 106. if (enclosingDropTarget != null) { 107. // ...set the enclosing drop target to null 108. // and notify the drop target that the drag 109. // source has exited 110. ds.setEnclosingDropTarget(null); 111. enclosingDropTarget.dragSourceExited(ds); 112. } 113. } 114. } 115. 116. public void onMouseUp(Widget sender, int x, int y) { 117. DragSource ds = (DragSource) sender; 118. Widget dsWidget = ds.getWidget(); 119. 120. if (!ds.isDragging()) { 121. // If the drag source is not being dragged, 122. // do nothing 123. return; 124. } 125. 126. DropTarget dt = intersectsDropTarget(dsWidget); 127. if (dt != null) { 128. // If the drag source intersects a drop target, 129. // notify the drop target that the drag source 130. // was dropped 131. dt.dragSourceDropped(ds); 132. } 133. else { 134. // If the drag source doesn't intersect a drop 135. // target, notify the drag source that it was 136. // dropped outside of any drop target 137. ds.droppedOutsideDropTarget(); 138. } 139. } 140. 141. private DropTarget intersectsDropTarget(Widget dsWidget) { 142. // Iterate over the collection of drop targets in the 143. // drag source's enclosing panel and see if the drag 144. // source intersects any of those drop targets; if so, 145. // return that drop target 146. Iterator it = dropTargets.iterator(); 147. while (it.hasNext()) { 148. DropTarget dt = (DropTarget) it.next(); 149. int dtLeft = dt.getAbsoluteLeft(); 150. int dtTop = dt.getAbsoluteTop(); 151. int dtWidth = dt.getOffsetWidth(); 152. int dtHeight = dt.getOffsetHeight(); 153. int dsLeft = dsWidget.getAbsoluteLeft(); 154. int dsTop = dsWidget.getAbsoluteTop(); 155. int dsWidth = dsWidget.getOffsetWidth(); 156. int dsHeight = dsWidget.getOffsetHeight(); 157. dsCorners[0] = new Point(dsLeft, dsTop); 158. dsCorners[1] = new Point(dsLeft + dsWidth, dsTop); 159. dsCorners[2] = 160. new Point(dsLeft + dsWidth, dsTop + dsHeight); 161. dsCorners[3] = new Point(dsLeft, dsTop + dsHeight); 162. 163. for (int i = 0; i < dsCorners.length; ++i) { 164. int x = dsCorners[i].x; 165. int y = dsCorners[i].y; 166. if (x > dtLeft && x < dtLeft + dtWidth && y > dtTop 167. && y < dtTop + dtHeight) { 168. return dt; 169. } 170. } 171. } 172. return null; 173. } 174.}
This is where most of the heavy lifting in the dnd module occurs. On a mouse down event, onMouseDown() finds all the drop targets in the drag source's enclosing absolute panel and stores them in an instance of WidgetCollection for further reference. That method also stores the drag source's location and invokes its startDragging method.
When the drag source is dragged, onMouseMove() checks to see if the drag source intersects one of the drop targets discovered in onMouseDown(); if so, it sets the drag source's enclosingDropTarget property and informs the drop target that a drag source has entered. If the drag source does not intersect a drop target but currently has an enclosing drop target, the listener informs the drop target that the drag source has exited and sets the drag source's enclosingDropTarget property to null.
When a drag source is dropped, either inside or outside a drop target, onMouseUp() informs both the drag source and drop target of the event.
Finally, notice that in onMouseEnter(), we call GWT's DOM.addEventPreview method to add an event preview to the top of the JavaScript event stack to prevent the browser from reacting to mouse drags. If we don't do that, then when a user drags an image, the browser will drag around an outline of the image as the user drags the mouse. It will not drag the image itself. Without that event preview, our drag and drop turns into mush (you might want to try removing the event preview and see the results for yourself). Subsequently, onMouseLeave() removes the event preview so that event handling returns to normal. See "Overriding a Pop-Up Panel's Default Event Handling Behavior" (page 211) for a more in-depth discussion of DOM.addEventPreview() and DOM.eventPreventDefault().
One final detail of our drag-and-drop module: The DragSourceListener class uses instances of the Component module's Point class, which is listed in Listing 6.16.
Listing 6.16. com.gwtsolutions.components.client.ui.Point
1.package com.gwtsolutions.components.client.ui; 2. 3.public class Point { // immutable 4. final public int x; 5. final public int y; 6. 7. public Point(int x, int y) { 8. this.x = x; 9. this.y = y; 10. } 11.}