2008/04/07

Drag and Drop the Tabs in JTabbedPane

Code

class DnDTabbedPane extends JTabbedPane {
  private static final int LINEWIDTH = 3;
  private static final String NAME = "test";
  private final GhostGlassPane glassPane = new GhostGlassPane();
  private final Rectangle lineRect  = new Rectangle();
  private final Color   lineColor = new Color(0, 100, 255);
  private int dragTabIndex = -1;

  private void clickArrowButton(String actionKey) {
    ActionMap map = getActionMap();
    if(map != null) {
      Action action = map.get(actionKey);
      if (action != null && action.isEnabled()) {
        action.actionPerformed(new ActionEvent(
            this, ActionEvent.ACTION_PERFORMED, null, 0, 0));
      }
    }
  }
  private static Rectangle rBackward = new Rectangle();
  private static Rectangle rForward  = new Rectangle();
  private static int rwh = 20;
  private static int buttonsize = 30;//XXX: magic number of scroll button size
  private void autoScrollTest(Point glassPt) {
    Rectangle r = getTabAreaBounds();
    int tabPlacement = getTabPlacement();
    if(tabPlacement==TOP || tabPlacement==BOTTOM) {
      rBackward.setBounds(r.x, r.y, rwh, r.height);
      rForward.setBounds(
          r.x+r.width-rwh-buttonsize, r.y, rwh+buttonsize, r.height);
    }else if(tabPlacement==LEFT || tabPlacement==RIGHT) {
      rBackward.setBounds(r.x, r.y, r.width, rwh);
      rForward.setBounds(
          r.x, r.y+r.height-rwh-buttonsize, r.width, rwh+buttonsize);
    }
    rBackward = SwingUtilities.convertRectangle(
        getParent(), rBackward, glassPane);
    rForward  = SwingUtilities.convertRectangle(
        getParent(), rForward,  glassPane);
    if(rBackward.contains(glassPt)) {
      //System.out.println(new java.util.Date() + "Backward");
      clickArrowButton("scrollTabsBackwardAction");
    }else if(rForward.contains(glassPt)) {
      //System.out.println(new java.util.Date() + "Forward");
      clickArrowButton("scrollTabsForwardAction");
    }
  }
  public DnDTabbedPane() {
    super();
    final DragSourceListener dsl = new DragSourceListener() {
      @Override public void dragEnter(DragSourceDragEvent e) {
        e.getDragSourceContext().setCursor(DragSource.DefaultMoveDrop);
      }
      @Override public void dragExit(DragSourceEvent e) {
        e.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop);
        lineRect.setRect(0,0,0,0);
        glassPane.setPoint(new Point(-1000,-1000));
        glassPane.repaint();
      }
      @Override public void dragOver(DragSourceDragEvent e) {
        Point glassPt = e.getLocation();
        SwingUtilities.convertPointFromScreen(glassPt, glassPane);
        int targetIdx = getTargetTabIndex(glassPt);
        //if(getTabAreaBounds().contains(tabPt) && targetIdx>=0 &&
        if(getTabAreaBounds().contains(glassPt) && targetIdx>=0 &&
           targetIdx!=dragTabIndex && targetIdx!=dragTabIndex+1) {
          e.getDragSourceContext().setCursor(DragSource.DefaultMoveDrop);
          glassPane.setCursor(DragSource.DefaultMoveDrop);
        }else{
          e.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop);
          glassPane.setCursor(DragSource.DefaultMoveNoDrop);
        }
      }
      @Override public void dragDropEnd(DragSourceDropEvent e) {
        lineRect.setRect(0,0,0,0);
        dragTabIndex = -1;
        glassPane.setVisible(false);
        if(hasGhost()) {
          glassPane.setVisible(false);
          glassPane.setImage(null);
        }
      }
      @Override public void dropActionChanged(DragSourceDragEvent e) {}
    };
    final Transferable t = new Transferable() {
      private final DataFlavor FLAVOR = new DataFlavor(
          DataFlavor.javaJVMLocalObjectMimeType, NAME);
      @Override public Object getTransferData(DataFlavor flavor) {
        return DnDTabbedPane.this;
      }
      @Override public DataFlavor[] getTransferDataFlavors() {
        DataFlavor[] f = new DataFlavor[1];
        f[0] = this.FLAVOR;
        return f;
      }
      @Override public boolean isDataFlavorSupported(DataFlavor flavor) {
        return flavor.getHumanPresentableName().equals(NAME);
      }
    };
    final DragGestureListener dgl = new DragGestureListener() {
      @Override public void dragGestureRecognized(DragGestureEvent e) {
        if(getTabCount() <= 1) return;
        Point tabPt = e.getDragOrigin();
        dragTabIndex = indexAtLocation(tabPt.x, tabPt.y);
        //"disabled tab problem".
        if(dragTabIndex < 0 || !isEnabledAt(dragTabIndex)) return;
        initGlassPane(e.getComponent(), e.getDragOrigin());
        try{
          e.startDrag(DragSource.DefaultMoveDrop, t, dsl);
        }catch(InvalidDnDOperationException idoe) {
          idoe.printStackTrace();
        }
      }
    };
    new DropTarget(glassPane, DnDConstants.ACTION_COPY_OR_MOVE,
                   new CDropTargetListener(), true);
    new DragSource().createDefaultDragGestureRecognizer(
          this, DnDConstants.ACTION_COPY_OR_MOVE, dgl);
  }

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

    private Point _glassPt = new Point();
    @Override public void dragOver(final DropTargetDragEvent e) {
      Point glassPt = e.getLocation();
      if(getTabPlacement()==JTabbedPane.TOP ||
         getTabPlacement()==JTabbedPane.BOTTOM) {
        initTargetLeftRightLine(getTargetTabIndex(glassPt));
      }else{
        initTargetTopBottomLine(getTargetTabIndex(glassPt));
      }
      if(hasGhost()) {
        glassPane.setPoint(glassPt);
      }
      if(!_glassPt.equals(glassPt)) glassPane.repaint();
      _glassPt = glassPt;
      autoScrollTest(glassPt);
    }

    @Override public void drop(DropTargetDropEvent e) {
      if(isDropAcceptable(e)) {
        convertTab(dragTabIndex, getTargetTabIndex(e.getLocation()));
        e.dropComplete(true);
      }else{
        e.dropComplete(false);
      }
      repaint();
    }
    private boolean isDragAcceptable(DropTargetDragEvent e) {
      Transferable t = e.getTransferable();
      if(t==null) return false;
      DataFlavor[] f = e.getCurrentDataFlavors();
      if(t.isDataFlavorSupported(f[0]) && dragTabIndex>=0) {
        return true;
      }
      return false;
    }
    private boolean isDropAcceptable(DropTargetDropEvent e) {
      Transferable t = e.getTransferable();
      if(t==null) return false;
      DataFlavor[] f = t.getTransferDataFlavors();
      if(t.isDataFlavorSupported(f[0]) && dragTabIndex>=0) {
        return true;
      }
      return false;
    }
  }

  private boolean hasGhost = true;
  public void setPaintGhost(boolean flag) {
    hasGhost = flag;
  }
  public boolean hasGhost() {
    return hasGhost;
  }
  private boolean isPaintScrollArea = true;
  public void setPaintScrollArea(boolean flag) {
    isPaintScrollArea = flag;
  }
  public boolean isPaintScrollArea() {
    return isPaintScrollArea;
  }

  private int getTargetTabIndex(Point glassPt) {
    Point tabPt = SwingUtilities.convertPoint(
        glassPane, glassPt, DnDTabbedPane.this);
    boolean isTB = getTabPlacement()==JTabbedPane.TOP ||
                   getTabPlacement()==JTabbedPane.BOTTOM;
    for(int i=0;i < getTabCount();i++) {
      Rectangle r = getBoundsAt(i);
      if(isTB) r.setRect(r.x-r.width/2, r.y,  r.width, r.height);
      else   r.setRect(r.x, r.y-r.height/2, r.width, r.height);
      if(r.contains(tabPt)) return i;
    }
    Rectangle r = getBoundsAt(getTabCount()-1);
    if(isTB) r.setRect(r.x+r.width/2, r.y,  r.width, r.height);
    else   r.setRect(r.x, r.y+r.height/2, r.width, r.height);
    return   r.contains(tabPt)?getTabCount():-1;
  }
  private void convertTab(int prev, int next) {
    if(next < 0 || prev==next) {
      return;
    }
    Component cmp = getComponentAt(prev);
    Component tab = getTabComponentAt(prev);
    String str  = getTitleAt(prev);
    Icon icon   = getIconAt(prev);
    String tip  = getToolTipTextAt(prev);
    boolean flg   = isEnabledAt(prev);
    int tgtindex  = prev>next ? next : next-1;
    remove(prev);
    insertTab(str, icon, cmp, tip, tgtindex);
    setEnabledAt(tgtindex, flg);
    //When you drag'n'drop a disabled tab, it finishes enabled and selected.
    //pointed out by dlorde
    if(flg) setSelectedIndex(tgtindex);

    //I have a component in all tabs (jlabel with an X to close the tab)
    //and when i move a tab the component disappear.
    //pointed out by Daniel Dario Morales Salas
    setTabComponentAt(tgtindex, tab);
  }

  private void initTargetLeftRightLine(int next) {
    if(next < 0 || dragTabIndex==next || next-dragTabIndex==1) {
      lineRect.setRect(0,0,0,0);
    }else if(next==0) {
      Rectangle r = SwingUtilities.convertRectangle(
          this, getBoundsAt(0), glassPane);
      lineRect.setRect(r.x-LINEWIDTH/2,r.y,LINEWIDTH,r.height);
    }else{
      Rectangle r = SwingUtilities.convertRectangle(
          this, getBoundsAt(next-1), glassPane);
      lineRect.setRect(r.x+r.width-LINEWIDTH/2,r.y,LINEWIDTH,r.height);
    }
  }
  private void initTargetTopBottomLine(int next) {
    if(next < 0 || dragTabIndex==next || next-dragTabIndex==1) {
      lineRect.setRect(0,0,0,0);
    }else if(next==0) {
      Rectangle r = SwingUtilities.convertRectangle(
          this, getBoundsAt(0), glassPane);
      lineRect.setRect(r.x,r.y-LINEWIDTH/2,r.width,LINEWIDTH);
    }else{
      Rectangle r = SwingUtilities.convertRectangle(
          this, getBoundsAt(next-1), glassPane);
      lineRect.setRect(r.x,r.y+r.height-LINEWIDTH/2,r.width,LINEWIDTH);
    }
  }

  private void initGlassPane(Component c, Point tabPt) {
    getRootPane().setGlassPane(glassPane);
    if(hasGhost()) {
      Rectangle rect = getBoundsAt(dragTabIndex);
      BufferedImage image = new BufferedImage(
          c.getWidth(), c.getHeight(), BufferedImage.TYPE_INT_ARGB);
      Graphics g = image.getGraphics();
      c.paint(g);
      rect.x = rect.x < 0?0:rect.x;
      rect.y = rect.y < 0?0:rect.y;
      image = image.getSubimage(rect.x,rect.y,rect.width,rect.height);
      glassPane.setImage(image);
    }
    Point glassPt = SwingUtilities.convertPoint(c, tabPt, glassPane);
    glassPane.setPoint(glassPt);
    glassPane.setVisible(true);
  }

  private Rectangle getTabAreaBounds() {
    Rectangle tabbedRect = getBounds();
    //pointed out by daryl. NullPointerException: i.e. addTab("Tab",null)
    //Rectangle compRect   = getSelectedComponent().getBounds();
    Component comp = getSelectedComponent();
    int idx = 0;
    while(comp==null && idx < getTabCount()) comp = getComponentAt(idx++);
    Rectangle compRect = (comp==null)?new Rectangle():comp.getBounds();
    int tabPlacement = getTabPlacement();
    if(tabPlacement==TOP) {
      tabbedRect.height = tabbedRect.height - compRect.height;
    }else if(tabPlacement==BOTTOM) {
      tabbedRect.y = tabbedRect.y + compRect.y + compRect.height;
      tabbedRect.height = tabbedRect.height - compRect.height;
    }else if(tabPlacement==LEFT) {
      tabbedRect.width = tabbedRect.width - compRect.width;
    }else if(tabPlacement==RIGHT) {
      tabbedRect.x = tabbedRect.x + compRect.x + compRect.width;
      tabbedRect.width = tabbedRect.width - compRect.width;
    }
    tabbedRect.grow(2, 2);
    return tabbedRect;
  }
  class GhostGlassPane extends JPanel {
    private final AlphaComposite composite;
    private Point location = new Point(0, 0);
    private BufferedImage draggingGhost = null;
    public GhostGlassPane() {
      setOpaque(false);
      composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f);
      //http://bugs.sun.com/view_bug.do?bug_id=6700748
      //setCursor(null);
    }
    public void setImage(BufferedImage draggingGhost) {
      this.draggingGhost = draggingGhost;
    }
    public void setPoint(Point location) {
      this.location = location;
    }
    @Override public void paintComponent(Graphics g) {
      Graphics2D g2 = (Graphics2D) g;
      g2.setComposite(composite);
      if(isPaintScrollArea() && getTabLayoutPolicy()==SCROLL_TAB_LAYOUT) {
        g2.setPaint(Color.RED);
        g2.fill(rBackward);
        g2.fill(rForward);
      }
      if(draggingGhost != null) {
        double xx = location.getX() - (draggingGhost.getWidth(this) /2d);
        double yy = location.getY() - (draggingGhost.getHeight(this)/2d);
        g2.drawImage(draggingGhost, (int)xx, (int)yy , null);
      }
      if(dragTabIndex>=0) {
        g2.setPaint(lineColor);
        g2.fill(lineRect);
      }
    }
  }
}

References

36 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Great job!!! Really!

    I have tried several tabbed pane in a same frame and in a same tab and it doesn't work... Maybe you could fix it or told me how to do this?

    Thanks in advance.

    ReplyDelete
  3. Hi percolala.

    Thank you for reporting the bug, which will now be fixed.
    - http://terai.xrea.jp/swing/dndtabbedpane/src.zip

    - frame.setGlassPane(glassPane);
    }

    private void initGlassPane(Component c, Point tabPt) {
    + getRootPane().setGlassPane(glassPane);
    if(hasGhost()) {

    ReplyDelete
  4. This works great! However, I notice that if you have the setTabLayoutPolicy(
    JTabbedPane.SCROLL_TAB_LAYOUT) value set that the blue rect does not get drawn on the sides of the tabs.

    ReplyDelete
  5. Hi todd.
    Thank you for your report and sorry for not being able to write to you sooner.
    "no effect on scrolling" is pointed out in percolala's mail and is investigating it now.
    so here is under construction example(work only "setTabPlacement(JTabbedPane.TOP)").
    DnDTabbedPane_AutoScroll_alpha.zip

    ReplyDelete
  6. Nice piece of code. Only it doesn't allow for disabled tabs. When you drag'n'drop a disabled tab, it finishes enabled and selected.

    The fix is to (in the tab move handler) set a flag for the tab enabled state, and keep a reference to the original selected tab component.

    When the tab move is done, set the selected component reference to the moved component if the tab was enabled, set the moved tab enable state according to the flag, and then select the tab using the selected component reference.

    ReplyDelete
  7. Hi dlorde.
    > disabled tab...
    This problem will be fixed soon.
    Thank you for your prompt & helpful response!

    ReplyDelete
  8. Hi.

    Greate code, it works awesome ...

    I have just one problem. I have a component (tabbedPane.setTabComponentAt(tabCount, cerrar);) in all tabs (jlabel with an X to close the tab) and when i move a tab the component disappear. Do you know how can i fix this ??

    thanks !!!

    ReplyDelete
  9. Hi, me again, i fixed the problem, it was easy ...

    In convertTab(int prev, int next) method under CDropTargetListener class, just added the next to code lines:

    Component tabCmp = getTabComponentAt(prev);

    setTabComponentAt(tgtindex, tabCmp);

    and that's all.

    greetings !!!

    ReplyDelete
  10. hi daniel.
    Your patch works fine.
    Thank you.

    ReplyDelete
  11. Works great!!!!
    One thing, if there is only one tab in the pane, it would be nice if it didnt start the drag operation since there is no place to move it to.
    thanks!!!

    ReplyDelete
  12. Hi derek_111.
    Thank you for your comment.

    > there is only one tab in the pane, didn't start the drag operation
    You can try to add this in void dragGestureRecognized(DragGestureEvent e) method:
    public void dragGestureRecognized(DragGestureEvent e) {
    // IE7, Chroeme like?
    if(getTabCount()<=1) return;
    ...

    ReplyDelete
  13. That worked perfectly!
    thanks!!!!

    ReplyDelete
  14. Another bug: Put a few tables in a few tabs, then take a tab and drag and drop to reorder it. Go back into any of the tabs and try to resize a table column. The cursor does not change to the resize cursor when you hover over the point between column headers. Weird?

    ReplyDelete
  15. Hi kostas. Thank you for reporting the bug.

    I think that may be a bug of this:
    JTable on a disabled JPanel get no Resize Cursor
    or (If you on Windows)
    Cursor flickering during D&D when using CellRendererPane with validation

    I'm not sure, bat changed the logic to use repaint() method.

    ReplyDelete
  16. Nice job aterai.

    In getTabAreaBounds(), if no component has been used (i.e., addTab("Tab",null)), then the statement:

    scr = getSelectedComponent().getBounds();

    fails. I worked around this by adding the following code:

    int idx = getSelectedIndex();
    Component comp = getSelectedComponent();
    comp = getTabComponentAt(idx);
    if (comp == null)
    scr = getBoundsAt(idx);
    else scr = getSelectedComponent().getBounds();

    ReplyDelete
  17. Hi daryl.
    I completely forgot about (component==null).
    I will fix that asap! Thank you.

    ReplyDelete
  18. i am not able to compile or run the code. Its not working. Plz give me the way of running this code.
    Reagards
    Shamsheer,

    ReplyDelete
  19. Hi shamsheer.
    I am using JDK 1.6.
    In order to run this sample with JDK 1.5 you need to remove or comment out line 283, 298:
    ////MainPanel.java:283: cannot find symbol
    //Component tab = getTabComponentAt(prev);
    ////MainPanel.java:298: cannot find symbol
    //setTabComponentAt(tgtindex, tab);

    Thanks

    ReplyDelete
  20. Great piece of code,
    I was searching for something like this and you implemented exactly the idea I had in my mind.
    Thank you very much

    Gianluca

    ReplyDelete
  21. Hi Gianluca. I'm glad I could help.

    ReplyDelete
  22. Nice class!!

    I have a problem: I have tab components with mouse listeners. The mouse listeners react to double-click to maximize the tab, and bring up context menus. However, they eat the mouse event and there is not drag event anymore.

    Is it possible to have both, mouse listeners on the tab components and dragging?

    Thanks, Alex

    ReplyDelete
  23. Hi, asac.cat.
    This might work:

    final JPopupMenu popup = new JPopupMenu();
    popup.add(new AbstractAction("test") {
    @Override public void actionPerformed(ActionEvent e) {
    System.out.println("maximize: "+tabbedPane.getSelectedIndex());
    }
    });
    //--------
    //How about using the dispatchEvent method:
    JLabel l = new JLabel("JTree 00");
    MouseAdapter ma = new MouseAdapter() {
    public void mousePressed(MouseEvent e) {
    if(e.getClickCount() >= 2) {
    popup.show(e.getComponent(), e.getX(), e.getY());
    }else{
    tabbedPane.dispatchEvent(SwingUtilities.convertMouseEvent(e.getComponent(), e, tabbedPane));
    }
    }
    public void mouseDragged(MouseEvent e) {
    tab.dispatchEvent(SwingUtilities.convertMouseEvent(e.getComponent(), e, tabbedPane));
    }
    };
    JLabel tab0 = new JLabel("tab0");
    tab0.addMouseListener(ma);
    tab0.addMouseMotionListener(ma);
    tabbedPane.setTabComponentAt(0, tab0);

    //--------
    //Or, as an alternate solution(use JTabbedPane#addMouseListener):
    tabbedPane.addMouseListener(new MouseAdapter() {
    public void mousePressed(MouseEvent e) {
    if(e.getClickCount() >= 2) popup.show(e.getComponent(), e.getX(), e.getY());
    }
    });

    Greetings and a happy new year.

    ReplyDelete
  24. Hey guys,

    this very cool and works great. I have a question, how hard would it be to implement Drag'n'Drop between two different TabbedPanes?

    ReplyDelete
  25. Hi, SpaceLovingAlien.
    How about this:
    http://terai.xrea.jp/swing/dndexporttabbedpane/src.zip

    ReplyDelete
  26. note:
    Bug/RFE fixed in JDK 6u21 build
    http://java-swing-tips.blogspot.com/2010/02/tabtransferhandler.html

    ReplyDelete
  27. Great work. What would be needed to change to get it working sharing tabs between 2 windows of the same application running twice? Like how Chrome does its tabs.

    ReplyDelete
  28. Hi,
    This is a good extension, but I get a "RasterFormatException" in the "initGlassPane()"-Method, if i have many Tabs and click the leftmost.
    It concerns the calling of "image.getSubimage( rect.x, rect.y, rect.width, rect.height );"

    ReplyDelete
  29. Hi, Dennis.
    Could you check if the `rect.x` and `rect.y` are non-negative value.

    ReplyDelete
  30. Thanks, rect.x is -2.
    Is it alright if I query for "less than 0"?

    ReplyDelete
  31. A quick fix to this problem is to add this line of code: rect.x = rect.x < 0 ? 0 : rect.x;
    (I guess, which has something to do with a particular Look&Feel "textShiftOffset"..., but not sure)

    ReplyDelete