// -*- mode:java; encoding:utf-8 -*- // vim:set fileencoding=utf-8: // @homepage@ //package example; package myutil; import java.awt.*; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.dnd.DnDConstants; import java.awt.dnd.DragGestureEvent; import java.awt.dnd.DragGestureListener; import java.awt.dnd.DragSource; import java.awt.dnd.DragSourceDragEvent; import java.awt.dnd.DragSourceDropEvent; import java.awt.dnd.DragSourceEvent; import java.awt.dnd.DragSourceListener; 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.dnd.InvalidDnDOperationException; import java.awt.image.BufferedImage; import java.util.Objects; import java.util.Optional; import java.util.stream.IntStream; import javax.swing.*; public class DraggableEnhancedTabbedPane extends JTabbedPane { private static final int LINEWIDTH = 3; private static final int RWH = 20; private static final int BUTTON_SIZE = 30; // XXX 30 is magic number of scroll button size private final GhostGlassPane glassPane = new GhostGlassPane(this); protected int dragTabIndex = -1; // For Debug: >>> protected boolean hasGhost = true; protected boolean isPaintScrollArea = true; // <<< protected Rectangle rectBackward = new Rectangle(); protected Rectangle rectForward = new Rectangle(); private DraggableTabbedPaneCallbackInterface callback; private int forbiddenDrag = -1; private void clickArrowButton(String actionKey) { JButton scrollForwardButton = null; JButton scrollBackwardButton = null; for (Component c: getComponents()) { if (c instanceof JButton) { if (Objects.isNull(scrollForwardButton) && Objects.isNull(scrollBackwardButton)) { scrollForwardButton = (JButton) c; } else if (Objects.isNull(scrollBackwardButton)) { scrollBackwardButton = (JButton) c; } } } JButton button = "scrollTabsForwardAction".equals(actionKey) ? scrollForwardButton : scrollBackwardButton; Optional.ofNullable(button) .filter(JButton::isEnabled) .ifPresent(JButton::doClick); // // ArrayIndexOutOfBoundsException // Optional.ofNullable(getActionMap()) // .map(am -> am.get(actionKey)) // .filter(Action::isEnabled) // .ifPresent(a -> a.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, null, 0, 0))); // // ActionMap map = getActionMap(); // // if (Objects.nonNull(map)) { // // Action action = map.get(actionKey); // // if (Objects.nonNull(action) && action.isEnabled()) { // // action.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, null, 0, 0)); // // } // // } } public void setForbiddenDrag(int index) { forbiddenDrag = index; } public void autoScrollTest(Point glassPt) { Rectangle r = getTabAreaBounds(); if (isTopBottomTabPlacement(getTabPlacement())) { rectBackward.setBounds(r.x, r.y, RWH, r.height); rectForward.setBounds(r.x + r.width - RWH - BUTTON_SIZE, r.y, RWH + BUTTON_SIZE, r.height); } else { rectBackward.setBounds(r.x, r.y, r.width, RWH); rectForward.setBounds(r.x, r.y + r.height - RWH - BUTTON_SIZE, r.width, RWH + BUTTON_SIZE); } rectBackward = SwingUtilities.convertRectangle(getParent(), rectBackward, glassPane); rectForward = SwingUtilities.convertRectangle(getParent(), rectForward, glassPane); if (rectBackward.contains(glassPt)) { clickArrowButton("scrollTabsBackwardAction"); } else if (rectForward.contains(glassPt)) { clickArrowButton("scrollTabsForwardAction"); } } public DraggableEnhancedTabbedPane(DraggableTabbedPaneCallbackInterface callback) { super(); this.callback = callback; glassPane.setName("GlassPane"); new DropTarget(glassPane, DnDConstants.ACTION_COPY_OR_MOVE, new TabDropTargetListener(), true); DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer( this, DnDConstants.ACTION_COPY_OR_MOVE, new TabDragGestureListener()); } protected int getTargetTabIndex(Point glassPt) { try { Point tabPt = SwingUtilities.convertPoint(glassPane, glassPt, this); Point d = isTopBottomTabPlacement(getTabPlacement()) ? new Point(1, 0) : new Point(0, 1); return IntStream.range(0, getTabCount()).filter(i -> { Rectangle r = getBoundsAt(i); r.translate(-r.width * d.x / 2, -r.height * d.y / 2); return r.contains(tabPt); }).findFirst().orElseGet(() -> { int count = getTabCount(); Rectangle r = getBoundsAt(count - 1); r.translate(r.width * d.x / 2, r.height * d.y / 2); return r.contains(tabPt) ? count : -1; }); // for (int i = 0; i < getTabCount(); i++) { // Rectangle r = getBoundsAt(i); // r.translate(-r.width * d.x / 2, -r.height * d.y / 2); // if (r.contains(tabPt)) { // return i; // } // } // Rectangle r = getBoundsAt(getTabCount() - 1); // r.translate(r.width * d.x / 2, r.height * d.y / 2); // return r.contains(tabPt) ? getTabCount() : -1; } catch (Exception e) { return 0; } } protected void convertTab(int prev, int next) { if (next < 0 || prev == next) { // This check is needed if tab content is null. return; } final Component cmp = getComponentAt(prev); final Component tab = getTabComponentAt(prev); final String title = getTitleAt(prev); final Icon icon = getIconAt(prev); final String tip = getToolTipTextAt(prev); final boolean isEnabled = isEnabledAt(prev); int tgtindex = prev > next ? next : next - 1; // Forbidden drag on this tab? if ((tgtindex == forbiddenDrag) || (prev == forbiddenDrag)) { return; } remove(prev); insertTab(title, icon, cmp, tip, tgtindex); if (callback != null) { callback.hasBeenDragged(prev, tgtindex); } setEnabledAt(tgtindex, isEnabled); // When you drag'n'drop a disabled tab, it finishes enabled and selected. // pointed out by dlorde if (isEnabled) { 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); } protected void initTargetLine(int next) { boolean isLeftOrRightNeighbor = next < 0 || dragTabIndex == next || next - dragTabIndex == 1; if (isLeftOrRightNeighbor) { glassPane.setTargetRect(0, 0, 0, 0); return; } Optional.ofNullable(getBoundsAt(Math.max(0, next - 1))).ifPresent(boundsRect -> { final Rectangle r = SwingUtilities.convertRectangle(this, boundsRect, glassPane); int a = Math.min(next, 1); // a = (next == 0) ? 0 : 1; if (isTopBottomTabPlacement(getTabPlacement())) { glassPane.setTargetRect(r.x + r.width * a - LINEWIDTH / 2, r.y, LINEWIDTH, r.height); } else { glassPane.setTargetRect(r.x, r.y + r.height * a - LINEWIDTH / 2, r.width, LINEWIDTH); } }); } protected void initGlassPane(Point tabPt) { getRootPane().setGlassPane(glassPane); if (hasGhost) { Component c = Optional.ofNullable(getTabComponentAt(dragTabIndex)) .orElseGet(() -> new JLabel(getTitleAt(dragTabIndex))); Dimension d = c.getPreferredSize(); BufferedImage image = new BufferedImage(d.width, d.height, BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = image.createGraphics(); SwingUtilities.paintComponent(g2, c, glassPane, 0, 0, d.width, d.height); g2.dispose(); glassPane.setImage(image); // Rectangle rect = getBoundsAt(dragTabIndex); // BufferedImage image = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); // Graphics2D g2 = image.createGraphics(); // paint(g2); // g2.dispose(); // if (rect.x < 0) { // rect.translate(-rect.x, 0); // } // if (rect.y < 0) { // rect.translate(0, -rect.y); // } // if (rect.x + rect.width > image.getWidth()) { // rect.width = image.getWidth() - rect.x; // } // if (rect.y + rect.height > image.getHeight()) { // rect.height = image.getHeight() - rect.y; // } // glassPane.setImage(image.getSubimage(rect.x, rect.y, rect.width, rect.height)); // // rect.x = Math.max(0, rect.x); // rect.x < 0 ? 0 : rect.x; // // rect.y = Math.max(0, 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(this, tabPt, glassPane); glassPane.setPoint(glassPt); glassPane.setVisible(true); } protected Rectangle getTabAreaBounds() { Rectangle tabbedRect = getBounds(); // XXX: Rectangle compRect = getSelectedComponent().getBounds(); // pointed out by daryl. NullPointerException: i.e. addTab("Tab", null) // Component comp = getSelectedComponent(); // int idx = 0; // while (Objects.isNull(comp) && idx < getTabCount()) { // comp = getComponentAt(idx++); // } Rectangle compRect = Optional.ofNullable(getSelectedComponent()) .map(Component::getBounds) .orElseGet(Rectangle::new); // // TEST: // Rectangle compRect = Optional.ofNullable(getSelectedComponent()) // .map(Component::getBounds) // .orElseGet(() -> IntStream.range(0, getTabCount()) // .mapToObj(this::getComponentAt) // .map(Component::getBounds) // .findFirst() // .orElseGet(Rectangle::new)); int tabPlacement = getTabPlacement(); if (isTopBottomTabPlacement(tabPlacement)) { tabbedRect.height = tabbedRect.height - compRect.height; if (tabPlacement == BOTTOM) { tabbedRect.y += compRect.y + compRect.height; } } else { tabbedRect.width = tabbedRect.width - compRect.width; if (tabPlacement == RIGHT) { tabbedRect.x += compRect.x + compRect.width; } } // 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; } public static boolean isTopBottomTabPlacement(int tabPlacement) { return tabPlacement == JTabbedPane.TOP || tabPlacement == JTabbedPane.BOTTOM; } } class TabTransferable implements Transferable { private static final String NAME = "test"; private static final DataFlavor FLAVOR = new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType, NAME); private final Component tabbedPane; protected TabTransferable(Component tabbedPane) { this.tabbedPane = tabbedPane; } @Override public Object getTransferData(DataFlavor flavor) { return tabbedPane; } @Override public DataFlavor[] getTransferDataFlavors() { return new DataFlavor[] {FLAVOR}; } @Override public boolean isDataFlavorSupported(DataFlavor flavor) { return flavor.getHumanPresentableName().equals(NAME); } } class TabDragSourceListener implements DragSourceListener { @Override public void dragEnter(DragSourceDragEvent e) { e.getDragSourceContext().setCursor(DragSource.DefaultMoveDrop); } @Override public void dragExit(DragSourceEvent e) { e.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop); // glassPane.setTargetRect(0, 0, 0, 0); // glassPane.setPoint(new Point(-1000, -1000)); // glassPane.repaint(); } @Override public void dragOver(DragSourceDragEvent e) { // Point glassPt = e.getLocation(); // JComponent glassPane = (JComponent) e.getDragSourceContext(); // SwingUtilities.convertPointFromScreen(glassPt, glassPane); // int targetIdx = getTargetTabIndex(glassPt); // 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) { // dragTabIndex = -1; // glassPane.setVisible(false); } @Override public void dropActionChanged(DragSourceDragEvent e) { /* not needed */ } } class TabDragGestureListener implements DragGestureListener { @Override public void dragGestureRecognized(DragGestureEvent e) { Optional.ofNullable(e.getComponent()) .filter(c -> c instanceof DraggableEnhancedTabbedPane).map(c -> (DraggableEnhancedTabbedPane) c) .filter(tabbedPane -> tabbedPane.getTabCount() > 1) .ifPresent(tabbedPane -> { Point tabPt = e.getDragOrigin(); tabbedPane.dragTabIndex = tabbedPane.indexAtLocation(tabPt.x, tabPt.y); if (tabbedPane.dragTabIndex >= 0 && tabbedPane.isEnabledAt(tabbedPane.dragTabIndex)) { tabbedPane.initGlassPane(tabPt); try { e.startDrag(DragSource.DefaultMoveDrop, new TabTransferable(tabbedPane), new TabDragSourceListener()); } catch (InvalidDnDOperationException ex) { throw new IllegalStateException(ex); } } }); } } class TabDropTargetListener implements DropTargetListener { private static final Point HIDDEN_POINT = new Point(0, -1000); private static Optional<GhostGlassPane> getGhostGlassPane(Component c) { return Optional.ofNullable(c).filter(GhostGlassPane.class::isInstance).map(GhostGlassPane.class::cast); } @Override public void dragEnter(DropTargetDragEvent e) { getGhostGlassPane(e.getDropTargetContext().getComponent()).ifPresent(glassPane -> { // DnDTabbedPane tabbedPane = glassPane.tabbedPane; Transferable t = e.getTransferable(); DataFlavor[] f = e.getCurrentDataFlavors(); if (t.isDataFlavorSupported(f[0])) { // && tabbedPane.dragTabIndex >= 0) { e.acceptDrag(e.getDropAction()); } else { e.rejectDrag(); } }); } @Override public void dragExit(DropTargetEvent e) { // Component c = e.getDropTargetContext().getComponent(); // System.out.println("DropTargetListener#dragExit: " + c.getName()); getGhostGlassPane(e.getDropTargetContext().getComponent()).ifPresent(glassPane -> { // XXX: glassPane.setVisible(false); glassPane.setPoint(HIDDEN_POINT); glassPane.setTargetRect(0, 0, 0, 0); glassPane.repaint(); }); } @Override public void dropActionChanged(DropTargetDragEvent e) { /* not needed */ } @Override public void dragOver(DropTargetDragEvent e) { Component c = e.getDropTargetContext().getComponent(); getGhostGlassPane(c).ifPresent(glassPane -> { Point glassPt = e.getLocation(); DraggableEnhancedTabbedPane tabbedPane = glassPane.tabbedPane; tabbedPane.initTargetLine(tabbedPane.getTargetTabIndex(glassPt)); tabbedPane.autoScrollTest(glassPt); glassPane.setPoint(glassPt); glassPane.repaint(); }); } @Override public void drop(DropTargetDropEvent e) { Component c = e.getDropTargetContext().getComponent(); getGhostGlassPane(c).ifPresent(glassPane -> { DraggableEnhancedTabbedPane tabbedPane = glassPane.tabbedPane; Transferable t = e.getTransferable(); DataFlavor[] f = t.getTransferDataFlavors(); int prev = tabbedPane.dragTabIndex; int next = tabbedPane.getTargetTabIndex(e.getLocation()); if (t.isDataFlavorSupported(f[0]) && prev != next) { tabbedPane.convertTab(prev, next); e.dropComplete(true); } else { e.dropComplete(false); } glassPane.setVisible(false); // tabbedPane.dragTabIndex = -1; }); } } class GhostGlassPane extends JComponent { private static final AlphaComposite ALPHA = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .5f); public final DraggableEnhancedTabbedPane tabbedPane; private final Rectangle lineRect = new Rectangle(); private final Color lineColor = new Color(0, 100, 255); private final Point location = new Point(); private transient Optional<BufferedImage> draggingGhostOp; protected GhostGlassPane(DraggableEnhancedTabbedPane tabbedPane) { super(); this.tabbedPane = tabbedPane; setOpaque(false); // [JDK-6700748] Cursor flickering during D&D when using CellRendererPane with validation - Java Bug System // https://bugs.openjdk.java.net/browse/JDK-6700748 // setCursor(null); } public void setTargetRect(int x, int y, int width, int height) { lineRect.setBounds(x, y, width, height); } public void setImage(BufferedImage draggingGhost) { this.draggingGhostOp = Optional.ofNullable(draggingGhost); } public void setPoint(Point pt) { this.location.setLocation(pt); } @Override public void setVisible(boolean v) { super.setVisible(v); if (!v) { setTargetRect(0, 0, 0, 0); setImage(null); } } @Override protected void paintComponent(Graphics g) { Graphics2D g2 = (Graphics2D) g.create(); g2.setComposite(ALPHA); if (tabbedPane.isPaintScrollArea && tabbedPane.getTabLayoutPolicy() == JTabbedPane.SCROLL_TAB_LAYOUT) { g2.setPaint(Color.RED); g2.fill(tabbedPane.rectBackward); g2.fill(tabbedPane.rectForward); } draggingGhostOp.ifPresent(img -> { double xx = location.getX() - img.getWidth(this) / 2d; double yy = location.getY() - img.getHeight(this) / 2d; g2.drawImage(img, (int) xx, (int) yy, null); }); g2.setPaint(lineColor); g2.fill(lineRect); g2.dispose(); } }