-
Ludovic Apvrille authoredLudovic Apvrille authored
DraggableEnhancedTabbedPane.java 17.93 KiB
// -*- 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();
}
}