package com.sun.java.forums; import java.awt.Color; import java.awt.Component; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.InputEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.swing.Icon; import javax.swing.JComponent; import javax.swing.JTabbedPane; import javax.swing.JViewport; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.event.EventListenerList; import javax.swing.plaf.basic.BasicTabbedPaneUI; /** * <p> * A {@code JTabbedPane} which has a close ('X') icon on each tab. * <p> * To add a tab, use the method addTab(String, Component) * <p> * To have an extra icon on each tab (e.g. like in JBuilder, showing the file * type) use the method addTab(String, Component, Icon). Only clicking the 'X' * closes the tab. * <p> * <b>Source</b>: <br> * <a href=http://forums.java.sun.com/thread.jspa?threadID=337070&start=15> Java * Forums - JTabbedPane with close icons, Post #15 </a> */ public class CloseableTabbedPane extends JTabbedPane implements MouseListener, MouseMotionListener { /** * */ private static final long serialVersionUID = 3881602564771126829L; /** * The {@code EventListenerList}. */ private EventListenerList myListenerList = null; /** * The viewport of the scrolled tabs. */ private JViewport headerViewport = null; /** * The normal closeicon. */ private Icon normalCloseIcon = null; /** * The closeicon when the mouse is over. */ private Icon hooverCloseIcon = null; /** * The closeicon when the mouse is pressed. */ private Icon pressedCloseIcon = null; /** * Creates a new instance of {@code CloseableTabbedPane}. */ public CloseableTabbedPane() { this(SwingConstants.LEFT); } /** * Creates a new instance of {@code CloseableTabbedPane}. * * @param horizontalTextPosition * the horizontal position of the text (e.g. * SwingUtilities.TRAILING or SwingUtilities.LEFT) */ public CloseableTabbedPane(final int horizontalTextPosition) { super(SwingConstants.TOP, JTabbedPane.SCROLL_TAB_LAYOUT); this.init(horizontalTextPosition); } @Override public void setSelectedIndex(final int index) { final int lastIndex = this.getSelectedIndex(); super.setSelectedIndex(index); if ((lastIndex != -1) && (lastIndex < this.getTabCount())) { this.setBackgroundAt(lastIndex, null); this.setForegroundAt(lastIndex, null); } this.setBackgroundAt(index, new Color(205, 205, 255)); this.setForegroundAt(index, Color.black); } /** * Initializes the {@code CloseableTabbedPane}. * * @param horizontalTextPosition * the horizontal position of the text (e.g. * SwingUtilities.TRAILING or SwingUtilities.LEFT) */ private void init(final int horizontalTextPosition) { this.myListenerList = new EventListenerList(); this.addMouseListener(this); this.addMouseMotionListener(this); this.setUI(new CloseableTabbedPaneUI(horizontalTextPosition)); } /** * Allows setting own closeicons. * * @param normal * the normal closeicon * @param hoover * the closeicon when the mouse is over * @param pressed * the closeicon when the mouse is pressed */ public void setCloseIcons(final Icon normal, final Icon hoover, final Icon pressed) { this.normalCloseIcon = normal; this.hooverCloseIcon = hoover; this.pressedCloseIcon = pressed; } /** * Adds a {@code Component} represented by a title and no icon. * * @param title * the title to be displayed in this tab * @param component * the component to be displayed when this tab is clicked */ @Override public void addTab(final String title, final Component component) { this.addTab(title, component, null); } /** * Adds a {@code Component} represented by a title and an icon. * * @param title * the title to be displayed in this tab * @param component * the component to be displayed when this tab is clicked * @param extraIcon * the icon to be displayed in this tab */ public void addTab(final String title, final Component component, final Icon extraIcon) { this.insertTab(title, extraIcon, component, title, this.getTabCount()); } @Override public void insertTab(String title, final Icon extraIcon, final Component component, final String tooltip, final int index) { boolean doPaintCloseIcon = true; try { title = " " + title + " "; Object prop = null; if ((prop = ((JComponent) component) .getClientProperty("isClosable")) != null) { doPaintCloseIcon = ((Boolean) prop).booleanValue(); } } catch (final Exception ignored) { // Could probably be a ClassCastException } component.addPropertyChangeListener("isClosable", new PropertyChangeListener() { @Override public void propertyChange(final PropertyChangeEvent e) { final Object newVal = e.getNewValue(); int index = -1; if (e.getSource() instanceof Component) { index = CloseableTabbedPane.this .indexOfComponent((Component) e.getSource()); } if ((index != -1) && (newVal != null) && (newVal instanceof Boolean)) { CloseableTabbedPane.this.setCloseIconVisibleAt( index, ((Boolean) newVal).booleanValue()); } } }); super.insertTab(title, doPaintCloseIcon ? new CloseTabIcon(extraIcon) : null, component, tooltip, index); if (this.headerViewport == null) { for (final Component c : this.getComponents()) { if ("TabbedPane.scrollableViewport".equals(c.getName())) { this.headerViewport = (JViewport) c; } } } } /** * Sets the closeicon at {@code index}. * * @param index * the tab index where the icon should be set * @param icon * the icon to be displayed in the tab * @throws IndexOutOfBoundsException * if index is out of range (index < 0 || index >= tab count) */ void setCloseIconVisibleAt(final int index, final boolean iconVisible) throws IndexOutOfBoundsException { super.setIconAt(index, iconVisible ? new CloseTabIcon(null) : null); } /** * Invoked when the mouse button has been clicked (pressed and released) on * a component. * * @param e * the {@code MouseEvent} */ @Override public void mouseClicked(final MouseEvent e) { this.processMouseEvents(e); } /** * Invoked when the mouse enters a component. * * @param e * the {@code MouseEvent} */ @Override public void mouseEntered(final MouseEvent e) { } /** * Invoked when the mouse exits a component. * * @param e * the {@code MouseEvent} */ @Override public void mouseExited(final MouseEvent e) { for (int i = 0; i < this.getTabCount(); i++) { final CloseTabIcon icon = (CloseTabIcon) this.getIconAt(i); if (icon != null) { icon.mouseover = false; } } this.repaint(); } /** * Invoked when a mouse button has been pressed on a component. * * @param e * the {@code MouseEvent} */ @Override public void mousePressed(final MouseEvent e) { this.processMouseEvents(e); } /** * Invoked when a mouse button has been released on a component. * * @param e * the {@code MouseEvent} */ @Override public void mouseReleased(final MouseEvent e) { } /** * Invoked when a mouse button is pressed on a component and then dragged. * {@code MOUSE_DRAGGED} events will continue to be delivered to the * component where the drag originated until the mouse button is released * (regardless of whether the mouse position is within the bounds of the * component). * <p> * Due to platform-dependent Drag&Drop implementations, * {@code MOUSE_DRAGGED} events may not be delivered during a native * Drag&Drop operation. * * @param e * the {@code MouseEvent} */ @Override public void mouseDragged(final MouseEvent e) { this.processMouseEvents(e); } /** * Invoked when the mouse cursor has been moved onto a component but no * buttons have been pushed. * * @param e * the {@code MouseEvent} */ @Override public void mouseMoved(final MouseEvent e) { this.processMouseEvents(e); } /** * Processes all caught {@code MouseEvent}s. * * @param e * the {@code MouseEvent} */ private void processMouseEvents(final MouseEvent e) { final int tabNumber = this.getUI().tabForCoordinate(this, e.getX(), e.getY()); if (tabNumber < 0) { return; } boolean otherWasOver = false; for (int i = 0; i < this.getTabCount(); i++) { if (i != tabNumber) { final CloseTabIcon ic = (CloseTabIcon) this.getIconAt(i); if (ic != null) { if (ic.mouseover) { otherWasOver = true; } ic.mouseover = false; } } } if (otherWasOver) { this.repaint(); } final CloseTabIcon icon = (CloseTabIcon) this.getIconAt(tabNumber); if (icon != null) { final Rectangle rect = icon.getBounds(); final boolean vpIsNull = this.headerViewport == null; final Point pos = vpIsNull ? new Point() : this.headerViewport .getViewPosition(); final int vpDiffX = vpIsNull ? 0 : this.headerViewport.getX(); final int vpDiffY = vpIsNull ? 0 : this.headerViewport.getY(); final Rectangle drawRect = new Rectangle( (rect.x - pos.x) + vpDiffX, (rect.y - pos.y) + vpDiffY, rect.width, rect.height); if (e.getID() == MouseEvent.MOUSE_PRESSED) { icon.mousepressed = e.getModifiers() == InputEvent.BUTTON1_MASK; this.repaint(drawRect); } else if ((e.getID() == MouseEvent.MOUSE_MOVED) || (e.getID() == MouseEvent.MOUSE_DRAGGED) || (e.getID() == MouseEvent.MOUSE_CLICKED)) { pos.x += e.getX() - vpDiffX; pos.y += e.getY() - vpDiffY; if (rect.contains(pos)) { if (e.getID() == MouseEvent.MOUSE_CLICKED) { final int selIndex = this.getSelectedIndex(); if (this.fireCloseTab(selIndex)) { if (selIndex > 0) { // to prevent uncatchable null-pointers final Rectangle rec = this.getUI() .getTabBounds(this, selIndex - 1); final MouseEvent event = new MouseEvent( (Component) e.getSource(), e.getID() + 1, System.currentTimeMillis(), e.getModifiers(), rec.x, rec.y, e.getClickCount(), e.isPopupTrigger(), e.getButton()); this.dispatchEvent(event); } // the tab is being closed // removeTabAt(tabNumber); this.remove(selIndex); } else { icon.mouseover = false; icon.mousepressed = false; this.repaint(drawRect); } } else { icon.mouseover = true; icon.mousepressed = e.getModifiers() == InputEvent.BUTTON1_MASK; } } else { icon.mouseover = false; } this.repaint(drawRect); } } } /** * Adds an {@code CloseableTabbedPaneListener} to the tabbed pane. * * @param l * the {@code CloseableTabbedPaneListener} to be added */ public void addCloseableTabbedPaneListener( final CloseableTabbedPaneListener l) { this.myListenerList.add(CloseableTabbedPaneListener.class, l); } /** * Removes an {@code CloseableTabbedPaneListener} from the tabbed pane. * * @param l * the listener to be removed */ public void removeCloseableTabbedPaneListener( final CloseableTabbedPaneListener l) { this.myListenerList.remove(CloseableTabbedPaneListener.class, l); } /** * Returns an array of all the {@code CloseableTabbedPaneListener}s added to * this {@code CloseableTabbedPane} with * {@link #addCloseableTabbedPaneListener()}. * * @return all of the {@code CloseableTabbedPaneListener}s added or an empty * array if no listeners have been added */ public CloseableTabbedPaneListener[] getCloseableTabbedPaneListeners() { return this.myListenerList .getListeners(CloseableTabbedPaneListener.class); } /** * Notifies all listeners that have registered interest for notification on * this event type. * * @param tabIndexToClose * the index of the tab which should be closed * @return true if the tab can be closed, false otherwise */ protected boolean fireCloseTab(final int tabIndexToClose) { boolean closeit = true; // Guaranteed to return a non-null array for (final Object listener : this.myListenerList.getListenerList()) { if (listener instanceof CloseableTabbedPaneListener) { if (!((CloseableTabbedPaneListener) listener) .closeTab(tabIndexToClose)) { closeit = false; break; } } } return closeit; } /** * The class which generates the 'X' icon for the tabs. The constructor * accepts an icon which is extra to the 'X' icon, so you can have tabs like * in JBuilder. This value is null if no extra icon is required. */ class CloseTabIcon implements Icon { /** * the x position of the icon */ private int x_pos; /** * the y position of the icon */ private int y_pos; /** * the width the icon */ private final int width; /** * the height the icon */ private final int height; /** * the additional fileicon */ private final Icon fileIcon; /** * true whether the mouse is over this icon, false otherwise */ private boolean mouseover = false; /** * true whether the mouse is pressed on this icon, false otherwise */ private boolean mousepressed = false; /** * Creates a new instance of {@code CloseTabIcon}. * * @param fileIcon * the additional fileicon, if there is one set */ public CloseTabIcon(final Icon fileIcon) { this.fileIcon = fileIcon; this.width = 16; this.height = 16; } /** * Draw the icon at the specified location. Icon implementations may use * the Component argument to get properties useful for painting, e.g. * the foreground or background color. * * @param c * the component which the icon belongs to * @param g * the graphic object to draw on * @param x * the upper left point of the icon in the x direction * @param y * the upper left point of the icon in the y direction */ @Override public void paintIcon(final Component c, final Graphics g, final int x, final int y) { boolean doPaintCloseIcon = true; try { // JComponent.putClientProperty("isClosable", new // Boolean(false)); final JTabbedPane tabbedpane = (JTabbedPane) c; final int tabNumber = tabbedpane.getUI().tabForCoordinate( tabbedpane, x, y); final JComponent curPanel = (JComponent) tabbedpane .getComponentAt(tabNumber); Object prop = null; if ((prop = curPanel.getClientProperty("isClosable")) != null) { doPaintCloseIcon = ((Boolean) prop).booleanValue(); } } catch (final Exception ignored) {/* * Could probably be a * ClassCastException */ } if (doPaintCloseIcon) { this.x_pos = x; this.y_pos = y; int y_p = y + 1; if ((CloseableTabbedPane.this.normalCloseIcon != null) && !this.mouseover) { CloseableTabbedPane.this.normalCloseIcon.paintIcon(c, g, x, y_p); } else if ((CloseableTabbedPane.this.hooverCloseIcon != null) && this.mouseover && !this.mousepressed) { CloseableTabbedPane.this.hooverCloseIcon.paintIcon(c, g, x, y_p); } else if ((CloseableTabbedPane.this.pressedCloseIcon != null) && this.mousepressed) { CloseableTabbedPane.this.pressedCloseIcon.paintIcon(c, g, x, y_p); } else { y_p++; final Color col = g.getColor(); if (this.mousepressed && this.mouseover) { g.setColor(Color.WHITE); g.fillRect(x + 1, y_p, 12, 13); } g.setColor(Color.black); /* * g.drawLine(x+1, y_p, x+12, y_p); g.drawLine(x+1, y_p+13, * x+12, y_p+13); g.drawLine(x, y_p+1, x, y_p+12); * g.drawLine(x+13, y_p+1, x+13, y_p+12); */ if (this.mouseover) { g.setColor(Color.GRAY); } g.drawLine(x + 4, y_p + 4, x + 9, y_p + 9); g.drawLine(x + 4, y_p + 5, x + 8, y_p + 9); g.drawLine(x + 5, y_p + 4, x + 9, y_p + 8); g.drawLine(x + 9, y_p + 4, x + 4, y_p + 9); g.drawLine(x + 9, y_p + 5, x + 5, y_p + 9); g.drawLine(x + 8, y_p + 4, x + 4, y_p + 8); g.setColor(col); if (this.fileIcon != null) { this.fileIcon.paintIcon(c, g, x + this.width, y_p); } } } } /** * Returns the icon's width. * * @return an int specifying the fixed width of the icon. */ @Override public int getIconWidth() { return this.width + (this.fileIcon != null ? this.fileIcon.getIconWidth() : 0); } /** * Returns the icon's height. * * @return an int specifying the fixed height of the icon. */ @Override public int getIconHeight() { return this.height; } /** * Gets the bounds of this icon in the form of a {@code Rectangle} * object. The bounds specify this icon's width, height, and location * relative to its parent. * * @return a rectangle indicating this icon's bounds */ public Rectangle getBounds() { return new Rectangle(this.x_pos, this.y_pos, this.width, this.height); } } /** * A specific {@code BasicTabbedPaneUI}. */ class CloseableTabbedPaneUI extends BasicTabbedPaneUI { /** * the horizontal position of the text */ private final int horizontalTextPosition; /** * Creates a new instance of {@code CloseableTabbedPaneUI}. */ public CloseableTabbedPaneUI() { this(SwingConstants.LEFT); } /** * Creates a new instance of {@code CloseableTabbedPaneUI} * * @param horizontalTextPosition * the horizontal position of the text (e.g. * SwingUtilities.TRAILING or SwingUtilities.LEFT) */ public CloseableTabbedPaneUI(final int horizontalTextPosition) { this.horizontalTextPosition = horizontalTextPosition; } /** * Layouts the label * * @param tabPlacement * the placement of the tabs * @param metrics * the font metrics * @param tabIndex * the index of the tab * @param title * the title of the tab * @param icon * the icon of the tab * @param tabRect * the tab boundaries * @param iconRect * the icon boundaries * @param textRect * the text boundaries * @param isSelected * true whether the tab is selected, false otherwise */ @Override protected void layoutLabel(final int tabPlacement, final FontMetrics metrics, final int tabIndex, final String title, final Icon icon, final Rectangle tabRect, final Rectangle iconRect, final Rectangle textRect, final boolean isSelected) { textRect.x = textRect.y = iconRect.x = iconRect.y = 0; final javax.swing.text.View v = this.getTextViewForTab(tabIndex); if (v != null) { this.tabPane.putClientProperty("html", v); } SwingUtilities.layoutCompoundLabel(this.tabPane, metrics, title, icon, SwingConstants.CENTER, SwingConstants.CENTER, SwingConstants.CENTER, this.horizontalTextPosition, tabRect, iconRect, textRect, this.textIconGap + 10); this.tabPane.putClientProperty("html", null); final int xNudge = this.getTabLabelShiftX(tabPlacement, tabIndex, isSelected); final int yNudge = this.getTabLabelShiftY(tabPlacement, tabIndex, isSelected); iconRect.x += xNudge; iconRect.y += yNudge; textRect.x += xNudge; textRect.y += yNudge; } } public void highlightTab(final int tabIndex) { this.setBackgroundAt(tabIndex, new Color(255, 205, 205)); this.setForegroundAt(tabIndex, Color.black); } @Override public boolean isFocusable() { return false; } @Override public final void requestFocus() { super.requestFocus(); this.transferFocus(); } @Override public final boolean requestFocus(final boolean temporary) { super.requestFocus(temporary); this.transferFocus(); return false; } @Override public final boolean requestFocusInWindow() { super.requestFocusInWindow(); this.transferFocus(); return false; } @Override public final boolean requestFocusInWindow(final boolean temporary) { super.requestFocusInWindow(temporary); this.transferFocus(); return false; } }