3

I've implemented a set of draggable tabs, following the form of this example: How to implement draggable tab using Java Swing?

Everything appears to work as I desire, however,when I drag outside of the main panel, the desktop will become a valid drop target (the resulting drop is accepted and marked successful).

Is there a way to intercept this drop to react to dropping outside of our root pane? It's simple enough to detect, but it's not clear to me how to actually capture the drop before the outside world does.

By the time DragSourceListener's dragDropEnd is called, the drop is already executed and there doesn't appear to be a good way to end dragging in dragOver/Exit/Whatever.

Gee, it'd be nice if something like this worked:

@Override
public void dragOver(DragSourceDragEvent dragEvent)
{
    DragEnabledTabTransferData data = getTabTransferData(dragEvent);
    DragSourceContext dragSourceContext = dragEvent.getDragSourceContext();
    if (data == null)
    {
        dragSourceContext.setCursor(DragSource.DefaultMoveNoDrop);
        return;
    }
    if (!data.getTabbedPane().getRootPane().getBounds().contains(dragEvent.getLocation()))
    {
        dragSourceContext.dragDropEnd(new DragSourceDropEvent(dragSourceContext, 999, true));
    }
}

Instead the drag continues dragging along. I do, however get a dragDropEnd for my troubles.

Any ideas? I'd be pretty sad to hear that the only solution would be to have some hidden maximized global pane that acted only as a drop target to capture out-of-window events.

Here is a working example. If you drag a tab out to, say, the desktop in Linux, it'll try to cast the transfer data into a Serializable and not be happy. The drag over I was playing with is commented with "This is where I'd assume we'd be able to intercept stuff" if you want to jump straight to what I'd pointed to above.

/** "Simple" example of DnD tabbed panes. Sourced from Eugene Yokota:
 * http:stackoverflow.com/questions/60269/how-to-implement-draggable-tab-using-java-swing */
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.dnd.*;
import javax.swing.*;

public class DnDTabbedPane extends JTabbedPane {
    private static final String NAME = "TabTransferData";
    private final DataFlavor FLAVOR = new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType, NAME);

    public DnDTabbedPane() {
        super();
        final DragSourceListener dsl = new DragSourceListener() {
            public void dragEnter(DragSourceDragEvent e) {
                e.getDragSourceContext().setCursor(DragSource.DefaultMoveDrop);
            }

            public void dragExit(DragSourceEvent e) {
                e.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop);
            }

            /**
             * This is where I'd assume we'd be able to intercept stuff 
             * so drops don't happen where we don't want them to.
             */
            public void dragOver(DragSourceDragEvent e) {
                TabTransferData data = getTabTransferData(e);
                if (data == null) {
                    e.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop);
                    return;
                }
                //This is where I ended up robokilling the drag via hackery
                e.getDragSourceContext().setCursor(DragSource.DefaultMoveDrop);
            }
            public void dragDropEnd(DragSourceDropEvent e) {}
            public void dropActionChanged(DragSourceDragEvent e) {}
        };

        final DragGestureListener dgl = new DragGestureListener() {
            public void dragGestureRecognized(DragGestureEvent e) {

                Point tabPt = e.getDragOrigin();
                int dragTabIndex = indexAtLocation(tabPt.x, tabPt.y);
                if (dragTabIndex < 0) {
                    return;
                }
                e.startDrag(DragSource.DefaultMoveDrop,new TabTransferable(DnDTabbedPane.this, dragTabIndex), dsl);
            }
        };

        new DropTarget(this, DnDConstants.ACTION_COPY_OR_MOVE, new CDropTargetListener(), true);
        new DragSource().createDefaultDragGestureRecognizer(this, DnDConstants.ACTION_COPY_OR_MOVE, dgl);
    }

    private TabTransferData getTabTransferData(DropTargetDropEvent a_event) {       
        try {
            return (TabTransferData) a_event.getTransferable().getTransferData(FLAVOR);             
        } catch (Exception e) {}

        return null;
    }

    private TabTransferData getTabTransferData(DropTargetDragEvent a_event) {
        try {
            return  (TabTransferData) a_event.getTransferable().getTransferData(FLAVOR);                
        } catch (Exception e) {}

        return null;
    }

    private TabTransferData getTabTransferData(DragSourceDragEvent a_event) {
        try {
            return (TabTransferData) a_event.getDragSourceContext().getTransferable().getTransferData(FLAVOR);              
        } catch (Exception e) {}

        return null;        
    }

    class TabTransferable implements Transferable {
        private TabTransferData m_data = null;
        private DataFlavor[] flavors = {FLAVOR};
        public TabTransferable(DnDTabbedPane a_tabbedPane, int a_tabIndex) {
            m_data = new TabTransferData(DnDTabbedPane.this, a_tabIndex);
        }

        public Object getTransferData(DataFlavor flavor) {
            return m_data;
        }

        public DataFlavor[] getTransferDataFlavors() {
            return flavors;
        }

        public boolean isDataFlavorSupported(DataFlavor flavor) {
            return flavor.getHumanPresentableName().equals(NAME);
        }       
    }

    class TabTransferData {
        DnDTabbedPane m_tabbedPane = null;
        int m_tabIndex = -1;

        public TabTransferData(DnDTabbedPane a_tabbedPane, int a_tabIndex) {
            m_tabbedPane = a_tabbedPane;
            m_tabIndex = a_tabIndex;
        }
    }

    class CDropTargetListener implements DropTargetListener {
        public void dragEnter(DropTargetDragEvent e) {
            if (isDragAcceptable(e)) {
                e.acceptDrag(e.getDropAction());
            } else {
                e.rejectDrag();
            }
        }

        public void drop(DropTargetDropEvent a_event) {
            if (isDropAcceptable(a_event)) {
                convertTab(getTabTransferData(a_event),
                getTargetTabIndex(a_event.getLocation()));
                a_event.dropComplete(true);
            } else {
                a_event.dropComplete(false);
            }
        }

        private boolean isTransferableGood(Transferable t, DataFlavor flavor)
        {
            return t == null || t.isDataFlavorSupported(flavor);
        }

        private boolean isDataGood(TabTransferData data)
        {
            if (DnDTabbedPane.this == data.m_tabbedPane && data.m_tabIndex >= 0) {
                return true;
            }
            return false;
        }

        public boolean isDragAcceptable(DropTargetDragEvent e) {
            Transferable t = e.getTransferable();
            if (!isTransferableGood(t, e.getCurrentDataFlavors()[0])) {
                return false;
            }
            return isDataGood(getTabTransferData(e));
        }

        public boolean isDropAcceptable(DropTargetDropEvent e) {
            Transferable t = e.getTransferable();
            if (!isTransferableGood(t, e.getCurrentDataFlavors()[0])) {
                return false;
            }
            return isDataGood(getTabTransferData(e));
        }

        public void dragExit(DropTargetEvent e) {}
        public void dropActionChanged(DropTargetDragEvent e) {}
        public void dragOver(final DropTargetDragEvent e) {}
    }

    private int getTargetTabIndex(Point a_point) {
        for (int i = 0; i < getTabCount(); i++) {
            Rectangle r = getBoundsAt(i);
            r.setRect(r.x - r.width / 2, r.y, r.width, r.height);
            if (r.contains(a_point)) {
                return i;
            }  
        }
        return -1;
    }

    private void convertTab(TabTransferData a_data, int a_targetIndex) {
        DnDTabbedPane source = a_data.m_tabbedPane;
        int sourceIndex = a_data.m_tabIndex;
        if (sourceIndex < 0) {
            return;
        }  

        Component cmp = source.getComponentAt(sourceIndex);
        String str = source.getTitleAt(sourceIndex);

        if (a_targetIndex < 0 || sourceIndex == a_targetIndex) {
            return;
        } 
        source.remove(sourceIndex);
        if (a_targetIndex == getTabCount()) {
            addTab(str, cmp);
        } else if (sourceIndex > a_targetIndex) {
            insertTab(str, null, cmp, null, a_targetIndex);
        } else {
            insertTab(str, null, cmp, null, a_targetIndex - 1);
        }
    }

    public static void main(String[] args)
    {
        JFrame window = new JFrame();
        DnDTabbedPane tabbedPane = new DnDTabbedPane();
        for(int i=0; i< 5; i++)
        {
            tabbedPane.addTab("I'm tab "+i, new JLabel("I'm tab "+i));
        }
        window.add(tabbedPane);
        window.setSize(400, 200);
        window.setVisible(true);
    }
}

Thus far, the best I can do is call something to this effect when we hop out of the parent.

    Component rootPane = SwingUtilities.getRoot(component);
    Rectangle bounds = rootPane.getBounds();
    if (!bounds.contains(location))
    {
        Robot robot = null;
        try
        {
            robot = new Robot();
        } catch (AWTException e)
        {
            return;
        }
        robot.keyPress(KeyEvent.VK_ESCAPE);
        robot.keyRelease(KeyEvent.VK_ESCAPE);
    }

It's a total hack, and doesn't solve my issue. I'd like to intercept the final drop event, see if it was outside of the frame and spawn the tab in its own JFrame.

If I was using the NetBeans, MyDoggy, or Eclipse frameworks, I guess this would all be magically handled for me. Alas.

Community
  • 1
  • 1
Amish
  • 71
  • 1
  • 5
  • For better help sooner, post an [SSCCE](http://sscce.org/). – Andrew Thompson Jan 10 '13 at 01:47
  • 600+ lines is not very 'short'. Can you trim it to under 300 and still show the effect? – Andrew Thompson Jan 11 '13 at 03:13
  • Heh. Yeah. I guess it's a bit bloated. Most of it is setting up the dragging behavior. I'll take a look in the morning and try to get something a bit more slimmed down. That said, I think I've already come to the conclusion that there's really no good solution to this aside from not using the built-in DnD API. Apparently, there's no good way to marshall who actually receives drops. – Amish Jan 11 '13 at 07:20
  • @AndrewThompson thanks for your input. I've torn out all the extras and gotten the example down to just above 200 lines. – Amish Jan 11 '13 at 18:20

3 Answers3

1

There is no Way to Cancel the Drag directly. see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4502185

I would prefer to show the User that Drop on Desktop is not allowed, by changing the Cursor.

Your DragSourceListener dsl has in the dragOver method a DragSourceDragEvent which tells you that the target action is NONE over the Desktop.

Change to this:

public void dragOver(DragSourceDragEvent e) {

    TabTransferData data = getTabTransferData(e);

    if( data == null || e.getTargetActions() == DnDConstants.ACTION_NONE ) {
        e.getDragSourceContext().setCursor( DragSource.DefaultMoveNoDrop );
        return;
    }

    e.getDragSourceContext().setCursor( DragSource.DefaultMoveDrop);
}

If you really want to Cancel, than you have to use your ESC solution or something like that:

    try {
        new Robot().mouseRelease( InputEvent.BUTTON1_MASK ); // if Button1 was the only Button to start a Drag
    } catch( AWTException e1 ) {
    }
oliholz
  • 7,447
  • 2
  • 43
  • 82
  • Yeah. That's basically what I've done. It's just a pain to have linux complain about the copy buffer being empty, or serializing the object our to a file if I don't ham-handedly cancel it. – Amish Jan 11 '13 at 17:39
0

As confirmed by @oliholz, there just isn't a way to do it without having to force a cancel via a keystroke.

However, for my needs of creating a tear-off tab, I found that creating a floating pane that was, itself, a drop target listener felt like the cleanest solution:

package com.amish.whatever;

import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JLabel;
import javax.swing.JWindow;
import javax.swing.Timer;

public class TearAwayTab extends JWindow {
    MousePoller mousePoller = new MousePoller();
    public TearAwayTab() {
        this.add(new JLabel("FLONT"));
        this.pack();
        new DropTarget(this, DnDConstants.ACTION_COPY_OR_MOVE, new EasyDropTarget(), true);
        this.setVisible(false);
    }

    private void center(Point location)
    {
        Point center = new Point();
        center.setLocation(location.x-this.getWidth()/2, location.y-this.getHeight()/2);
        TearAwayTab.this.setLocation(center);
    }

    public void attach(Point location)
    {
        center(location);
        mousePoller.start();
        this.setVisible(true);
    }

    public void detach()
    {
        mousePoller.stop();
        this.setVisible(false);
    }

    private int DELAY = 10;
    private class MousePoller extends Timer{
        public MousePoller(){
            super(DELAY, new ActionListener() {
                private Point lastPoint = MouseInfo.getPointerInfo().getLocation();
                @Override
                public void actionPerformed(ActionEvent e) {
                    Point point = MouseInfo.getPointerInfo().getLocation();

                    if (!point.equals(lastPoint)) {
                        center(point);
                    }

                    lastPoint = point;
                }
            });
        }
    }

    private class EasyDropTarget implements DropTargetListener
    {

        @Override
        public void dragEnter(DropTargetDragEvent dtde) {
            dtde.acceptDrag(dtde.getDropAction());
        }

        @Override
        public void dragOver(DropTargetDragEvent dtde) {}

        @Override
        public void dropActionChanged(DropTargetDragEvent dtde) {}

        @Override
        public void dragExit(DropTargetEvent dte) {}

        @Override
        public void drop(DropTargetDropEvent dtde) {
            dtde.dropComplete(true);
            detach();
            System.out.println("DROP Intercepted");
        }
    }
}

The bit with the MousePoller works around scrubbing the mouse too fast for mouse listeners to reliably update the location. I'd tried with a motion listener and was able to escape the bounds of the floater quite easily.

Back in the first example, I now include the tear away tab as a private member of the tabbed pane, and call attach and detach when exiting or entering my drop areas:

        final DragSourceListener dsl = new DragSourceListener() {
            public void dragEnter(DragSourceDragEvent e) {
                e.getDragSourceContext().setCursor(DragSource.DefaultMoveDrop);
                Rectangle bounds = SwingUtilities.getRoot(DnDTabbedPane.this).getBounds();

                if(bounds.contains(e.getLocation())){
                    tearTab.detach(); 
                }
            }

            public void dragExit(DragSourceEvent e) {
                e.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop);
                tearTab.attach(e.getLocation());
            }
            ...

This also has the added benefit of preserving the DnD operation in the case of dragging out, and then back in.

Thanks for the input. If you have any other ideas/comments, I'm all ears.

Amish
  • 71
  • 1
  • 5
  • Mouse Stuff was via @Thomas from here: http://stackoverflow.com/questions/2469515/java-mouse-motion-anywhere-on-screen – Amish Jan 14 '13 at 19:29
0

This doesn't directly relate to tabs, but one way to stop drags from being able to be dragged to the desktop is to wrap whatever you're dragging in a custom wrapper class. Then, when you make your TransferHandler, make a DataFlavor localFlavor = new ActivationDataFlavor(YourWrapperClass.class, DataFlavor.javaJVMLocalObjectMimeType, "description"); Next, override the createTransferable method to have new DataHandler(passedInComponent, localFlavor.getMimeType()); and return a new Transferable in which you've overridden all the methods to only have your localFlavor. Finally, in the importData method, make sure to import your data as your localFlavor type. This will prevent dragging to the deaktop as the flavor you defined is local to the JVM.

Gossamer Shadow
  • 387
  • 1
  • 5
  • 16
  • A good example of this can be found in aterai's answer to this [link](http://stackoverflow.com/questions/11460704/dragging-a-jlabel-with-a-transferhandler-drag-and-drop). Particularly look at the commented out part of the code... note that aterai left in the other DataFlavors rather than taking them out, but deleting a few lines resolves this. – Gossamer Shadow Jan 18 '13 at 18:37