Monday, November 12, 2012

GWT(2.4+) Drag & Drop in a Tree

For an interface I am working on for Green Motion Travel, I needed to create a file organizer.

The files are presented in a tree, and the organizer allows you to drag files and folders around within the tree.

To accomplish this, I decided to use GWT's new Drag And Drop features.

I couldn't find any real documentation on how to use it, but there is a video on the subject on youtube, here:
    Google I/O 2011: GWT + HTML5: A web developers dream!
The bit on Drag&Drop starts at about 21 minutes in.
It turned out to be fairly easy!

Where to start?

With GWT, you can add Drag & Drop to almost anything.
I wanted to add D&D to TreeItems.  As it turns out, TreeItems don't really work well as D&D targets.  The target somehow ends up being the tree itself, so that you can't really drop a file onto another treeitem.

So, instead of adding D&D to the TreeItem objects, I instead created HTML objects and set those as widgets in the tree, and then added D&D to those.  Labels would work too, of course, but I wanted to add icons to represent folders and files.  For this tutorial, I will just use Labels, as it is a bit cleaner.

To make a widget draggable, you must do the following:
  • Set the 'draggable' property
  • Add a drag start handler
Some widgets support a draggable property directly, however Label does not. So, we need to go to the element and set it there.
In the Drag Start Handler, you must set the event data, or some browsers will not allow you to drag it.
In my case, I don't really care what the data is.  Instead, I will set a class field member (called 'dragging') to remember what label is being dragged.
Finally, we will copy an image of the existing label as the drag image.

Making a Label Draggable

Label draggableLabel = new Label("drag me!");
dragLabel .getElement().setDraggable(Element.DRAGGABLE_TRUE);
dragLabel.addDragStartHandler(new DragStartHandler()
{
  @Override
  public void onDragStart(DragStartEvent event)
  {
    // Remember what's being dragged
    dragging = dragLabel;
    // Must set anyway for FireFox
    event.setData("text", "hi there");
    // Copy the label as the drag image
    event.getDataTransfer().setDragImage(getElement(), 10, 10);
  }
});

If you were looking closely, you might have noticed that dragLabel is not final, as it would have to be in the above code.  I didn't bother, as I have wrapped everything up into a class that extends Label, as you will see below.

To make a widget droppable, you must:
  • Add a DragOver handler
  • Add a Drop handler
Again, some widgets support dropping in the API, however Label does not.  So instead, you need to add it using addDomHandler.  No problem.
The DragOver handler doesn't have to do anything.  I will use it, however, to apply a style to the label.
To counter the drag-over style, I will also add a DragLeave event.

Making a Label Droppable

Label dropLabel = new Label("Drop onto me");
dropLabel.addDomHandler(new DragOverHandler()
{
    @Override
    public void onDragOver(DragOverEvent event)
    {
        dropLabel.addStyleName("dropping");
    }
}, DragOverEvent.getType());

dropLabel.addDomHandler(new DragLeaveHandler()
{
    @Override
    public void onDragLeave(DragLeaveEvent event)
    {
        dropLabel.removeStyleName("dropping");
    }
}, DragLeaveEvent.getType());

dropLabel.addDomHandler(new DropHandler()
{
    @Override
    public void onDrop(DropEvent event)
    {
        event.preventDefault();
        // Do something with dropLabel and dragging
        etc...
    }
}, DropEvent.getType());

BIG NOTE:  Make sure to call event.preventDefault() in the onDrop, or else the browser might navigate away from the current page!

That's about it for making my labels draggable and droppable.
For my situation, I needed to create:
  • Leaf nodes for files, that are draggable and not droppable.
  • Root folders that are droppable, but not draggable
  • Subfolders that are both draggable and droppable.
I wrapped all of the above up into a class, called DragDropLabel.
The DropHandler for this example will move the TreeItem of the source under the TreeItem of the drop target.
Here is the implementation of my DragDropLabel class:

class DragDropLabel extends Label 
{
     private static DragDropLabel dragging = null;
     final boolean droppable;
     public DragDropLabel(String text, boolean draggable, boolean droppable)
     {
         super(text);
         if (draggable)
         {
             initDrag();
         }
         if (droppable)
         {
             initDrop();
         }
         this.droppable = droppable;
         if (droppable)
         {
             addStyleName("droppable");
         }
         else if (draggable)
         {
             addStyleName("draggable");
         }
     }
    
     private void initDrag()
     {
         getElement().setDraggable(Element.DRAGGABLE_TRUE);
         addDragStartHandler(new DragStartHandler()
         {
             @Override
             public void onDragStart(DragStartEvent event)
             {
                 // Remember what's being dragged
                 dragging = DragDropLabel.this;
                 // Must set for FireFox
                 event.setData("text", "hi there");

                // Copy the label image for the drag icon
                // 10,10 indicates the pointer offset, not the image size.
                event.getDataTransfer().setDragImage(getElement(), 10, 10);
             }
         });
    }

     private void initDrop()
     {
         addDomHandler(new DragOverHandler()
         {
             @Override
             public void onDragOver(DragOverEvent event)
             {
                 addStyleName("dropping");
             }
         }, DragOverEvent.getType());

         addDomHandler(new DragLeaveHandler()
         {
             @Override
             public void onDragLeave(DragLeaveEvent event)
             {
                 removeStyleName("dropping");
             }
         }, DragLeaveEvent.getType());

         addDomHandler(new DropHandler()
         {
             @Override
             public void onDrop(DropEvent event)
             {
                 event.preventDefault();
                 if (dragging != null)
                 {
                     // Target treeitem is found via 'this';
                     // Dragged treeitem is found via 'dragging'.

                     TreeItem dragTarget = null;
                     TreeItem dragSource = null;
                     // The parent of 'this' is not the TreeItem, as that's not a Widget.
                     // The parent is the tree containing the treeitem.
                     Tree tree = (Tree)DragDropLabel.this.getParent();
 
                     // Visit the entire tree, searching for the drag and drop TreeItems
                     List<TreeItem> stack = new ArrayList<TreeItem>();
                     stack.add(tree.getItem(0));
                     while(!stack.isEmpty())
                     {
                         TreeItem item = stack.remove(0);
                         for(int i=0;i<item.getChildCount();i++)
                         {
                             stack.add(item.getChild(i));
                         }

                         Widget w = item.getWidget();
                         if (w != null)
                         {
                             if (w == dragging)
                             {
                                 dragSource = item;
                                 if (dragTarget != null)
                                 {
                                     break;
                                 }
                             }
                             if (w == DragDropLabel.this)
                             {
                                 dragTarget = item;
                                 w.removeStyleName("dropping");
                                 if (dragSource != null)
                                 {
                                     break;
                                 }
                             }
                         }
                     }
                     if (dragSource != null && dragTarget != null)
                     {
                         // Make sure that target is not a child of dragSource

                         TreeItem test = dragTarget;
                         while(test != null)
                         {
                             if (test == dragSource)
                             {
                                 return;
                             }
                             test = test.getParentItem();
                         }
                         dragTarget.addItem(dragSource);
                         dragTarget.setState(true);
                     }
                     dragging = null;
                 }
             }
         }, DropEvent.getType());
     }
 }


And so, with that class, now all I have to do is add some of them to a tree, like so:
Tree tree = new Tree();
RootPanel.get("main").add(tree);
  
// root is not draggable.
TreeItem root = new TreeItem(new DragDropLabel("root", false, true));
tree.addItem(root);
  
// Add some draggable and droppable folders to 'root'
root.addItem(new DragDropLabel("folder1", true, true));
root.addItem(new DragDropLabel("folder2", true, true));

etc...

Here is a screenshot of the tree in action, dragging a file onto a folder.



You can download the source code for the example here.
Select "File/Download" to save it.
Note that it does not contain the GWT libs to save space.

Please leave a comment if this has helped you out!


Thursday, November 8, 2012

GWT Auto Logout & Open Dialogs

Currently, I work for CounterPath, developing a large GWT application (as well as doing some GWT work for Green Motion Travel).

At CounterPath, my GWT application has an auto-logout feature.
This is to avoid the situation where an application user doesn't do anything for a while, and then edits a pile of information and clicks 'Save', only to find out their session timer has expired, meaning they just lost their work.

The auto-logout feature keeps an internal timer that matches the server's session timeout.
The internal timer is reset every time I do some server interaction.
To avoid inserting these calls all over the place in my code, I added an Async RPC facade;  My application uses the facade, and the facade makes the call to the server.
The downside is that I have to maintain another copy of the RPC interface.  Fortunately, that part is quite stable.

For example, I have:
  • MyData - the RPC interface
  • MyDataAsync - the Async version of MyData
and now,
  • MyDataAsyncSession - a class that implements MyDataAsync

MyData:

public interface MyData extends RemoteService
{
    public Data getData();
}

MyDataAsync:

public interface MyDataAsync
{
    public void getData(AsyncCallback<Data> callback);
    etc...
}

And, in my client code,

MyDataAsyncSession:

public class MyDataAsyncSession implements MyDataAsync
{
    MyDataAsync realRPC = (MyDataAsync)GWT.create(MyData.class);

    @Override
    public void getData(AsyncCallback<Data> result)
    {
        resetSessionTimer();
        realRPC.getData(result);
    }

    etc...
In my actual client code, I would then have:
MyDataAsyncSession rpc = new MyDataAsyncSession();
rpc.getData(new AsyncCallback<Data>(){...});
In this way, I don't have to worry about sprinkling calls to resetSessionTimer all over my code.
If this was all on the server, I could probably use AOP to insert calls to resetSessionTimer() in the right places...

On to the main point of this post.
In a few places, I use popup dialogs.
This leads to a problem: If you do something that generates a popup dialog, and then let it sit there for 30 minutes, the GWT application will auto-log-out, which will hide the other panels and show the login panel.  However, it won't hide the popup dialogs!  They will continue to show whatever was being worked on.

At first, I was considering a popup registration system, maintaining a list of popups that are currently open.  This could get messy, with some popups being auto-close, and others closed with a button or other control.
Fortunately, I found an easier way:  Just search the default root panel for PopupPanel widgets, and close them!

Here is the code, which is now part of my doLogout() function:

Code to Auto-Close Popup Panels

// Create a widget processing list, and
  // add the default root panel to it.
  List widgetList = new ArrayList();
  widgetList.add(RootPanel.get());
  while(!widgetList.isEmpty())
  {
      // Pull the first widget from the list
      Widget w = widgetList.remove(0);

      // If it is a popup, hide it!
      if (w instanceof PopupPanel)
      {
          ((PopupPanel)w).hide();
      }
      else if (w instanceof HasWidgets)
      {
          // Add any child widgets to the processing list.
          Iterator iter = ((HasWidgets)w).iterator();
          while(iter.hasNext())
          {
              Widget child = iter.next();
              widgetList.add(child);
          }
      }
  }

That's a lot easier than having to maintain a list of open popups!