package chatty.gui.components.tabs;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.image.BufferedImage;
import javax.swing.Icon;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
/**
* Allow tabs to be dragged to be repositioned.
*
* This probably won't work very well if the tabs aren't oriented horizontally
* and ok in most cases but not quite perfectly if they are placed in more than
* one row.
*
* Based on: http://stackoverflow.com/a/60306/2375667 (but extended quite a bit)
*
* @author tduva
*/
public class DraggableTabbedPane extends JTabbedPane {
/**
* How long after the drag started should the dragging become visible and
* take effect. This is to prevent accidently dragging while just clicking
* on the tab while moving the mouse slightly, which can lead to the
* hovering tab show up for a fraction of a second.
*
* It is only the painting of the hovering tab and the actual drop that is
* prevented when the drag is not long enough, the drag is still started in
* the background in case it continues, so it already has started on the
* correct tab and not the one the mouse was over when the threshold was
* satisfied.
*/
private static final int DRAG_STARTED_THRESHOLD = 100;
private boolean dragging;
private int draggedTabIndex;
private Image draggedTabImage;
private int draggedTabImageHeight;
private Point mouseLocation;
private boolean canDrag;
private long dragStarted;
public DraggableTabbedPane() {
super();
addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
if (!canDrag) {
return;
}
if (dragStarted == 0) {
dragStarted = System.currentTimeMillis();
}
mouseLocation = e.getPoint();
if (!dragging) {
int tabIndex = getIndexForPoint(e.getPoint());
if (tabIndex >= 0) {
draggedTabIndex = tabIndex;
Rectangle bounds = getBoundsAt(tabIndex);
Image totalImage = new BufferedImage(getWidth(),
getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics totalGraphics = totalImage.getGraphics();
totalGraphics.setClip(bounds);
setDoubleBuffered(false);
paintComponent(totalGraphics);
draggedTabImageHeight = bounds.height;
draggedTabImage = new BufferedImage(bounds.width,
bounds.height, BufferedImage.TYPE_INT_ARGB);
Graphics g = draggedTabImage.getGraphics();
g.drawImage(totalImage, 0, 0,
bounds.width, bounds.height,
bounds.x, bounds.y,
bounds.x+bounds.width, bounds.y+bounds.height,
DraggableTabbedPane.this);
dragging = true;
}
}
repaint();
}
});
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
dragStarted = 0;
/**
* Only allow a drag to start using the left mouse button. Can't
* use isPopupTrigger() to prevent it from dragging when opening
* a context menu, because the popup trigger isn't necessarily
* available in mousePressed().
*/
canDrag = SwingUtilities.isLeftMouseButton(e);
}
@Override
public void mouseReleased(MouseEvent e) {
if (dragging && dragStartedThreshold()) {
int dropOnIndex = getDropIndexForPoint(e.getPoint());
if (dropOnIndex >= 0) {
moveTab(draggedTabIndex, dropOnIndex);
}
}
dragging = false;
draggedTabImage = null;
dragStarted = 0;
repaint();
}
});
addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
dragging = false;
dragStarted = 0;
repaint();
}
});
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (dragging && mouseLocation != null && draggedTabImage != null && dragStartedThreshold()) {
// Draw floating tab image
g.drawImage(draggedTabImage, mouseLocation.x, mouseLocation.y - draggedTabImageHeight / 2, this);
/**
* Draw indicator where the tab would be dropped onto. This might
* not work very well if the tabs are layed out in more than one
* row or are not placed horizontally.
*/
int tabIndex = getDropIndexForPoint(mouseLocation);
if (tabIndex >= 0 && tabIndex != draggedTabIndex
&& tabIndex != draggedTabIndex+1) {
boolean drawBeforeTab = true;
if (tabIndex >= getTabCount()) {
// If after last tab, then draw behind the last tab
tabIndex = getTabCount() - 1;
drawBeforeTab = false;
}
Rectangle bounds = getUI().getTabBounds(DraggableTabbedPane.this, tabIndex);
if (drawBeforeTab) {
g.fillRect(bounds.x, bounds.y, 3, bounds.height);
} else {
g.fillRect(bounds.x+bounds.width-3, bounds.y, 3, bounds.height);
}
}
}
}
/**
* Returns the time in milliseconds how long ago the current drag started.
*
* @return The number of milliseconds since the current drag started, or -1
* if no drag is currently in progress
*/
private long dragStartedAgo() {
if (dragStarted == 0) {
return -1;
}
return System.currentTimeMillis() - dragStarted;
}
/**
* Check if the current drag has exceeded the DRAG_STARTED_THRESHOLD, which
* prevents accidental dragging.
*
* @return true if the current drag should be considered as actually
* started, false otherwise or if no drag is currently in progress
*/
private boolean dragStartedThreshold() {
return dragStartedAgo() > DRAG_STARTED_THRESHOLD;
}
/**
* Move a tab from a given index to another.
*
* @param from The index of the tab to move
* @param to The index to move the tab to
* @throws IndexOutOfBoundsException if from or to are not in the range of
* tab indices
*/
private void moveTab(int from, int to) {
if (from == to) {
// Nothing to do here
return;
}
Component comp = getComponentAt(from);
String title = getTitleAt(from);
String toolTip = getToolTipTextAt(from);
Icon icon = getIconAt(from);
Component tabComp = getTabComponentAt(from);
removeTabAt(from);
/**
* If the source index (the tab that is moved) is in front of the target
* index, then removing the source index will have shifted the target
* index back by one, so adjust for that.
*/
if (from < to) {
to--;
}
insertTab(title, icon, comp, toolTip, to);
setTabComponentAt(to, tabComp);
setSelectedComponent(comp);
}
private int getIndexForPoint(Point p) {
return indexAtLocation(p.x, p.y);
}
/**
* Get the drop index for the given location. The drop index is the location
* between the tabs to insert the dragged tab into. Basicially it's the
* index of the tab to insert the dragged tab in front of, or the last tab
* index + 1 if it should be inserted after the last tab.
*
* @param p The location to find the drop index for
* @return The drop index (> 0 and <= tab count), or -1 if none could be
* found
*/
private int getDropIndexForPoint(Point p) {
int index = getIndexForPoint(p);
if (index >= 0) {
Rectangle bounds = getBoundsAt(index);
if (p.x < bounds.x + bounds.width / 2) {
return index;
} else {
return index+1;
}
} else {
/**
* Basicially making the last tab wider to have more leeway with
* dropping it after the last tab.
*/
Rectangle bounds = getBoundsAt(getTabCount() - 1);
bounds.width += 30;
if (bounds.contains(p)) {
return getTabCount();
}
}
return -1;
}
}