Home > Articles > Home & Office Computing > eBay/Yahoo/Google

  • Print
  • + Share This
This chapter is from the book

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

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.}
  • + Share This
  • 🔖 Save To Your Account