CloseAndMaxTabbedPane: An enhanced JTabbedPane

Add Close and Maximize buttons to your tabbed pane

Professional Java Swing-based applications implement custom Swing components to enhance their functionality and usability. For example, IDEs like Eclipse and NetBeans use an enhanced tabbed pane as a container for source code editors. This tabbed pane has a Close icon on each tab that lets you easily close individual tabs.

In this article, we present the implementation of CloseAndMaxTabbedPane, an enhanced tabbed pane with Close and Maximize buttons. The component's look and feel is similar to Eclipse 2.1. You can also selectively enable/disable the Close or Maximize buttons as necessary. Figure 1 illustrates the screenshot of CloseAndMaxTabbedPane.

Figure 1. CloseAndMaxTabbedPane in action. Click on thumbnail to view full-size image.

Implement CloseAndMaxTabbedPane

The UML diagram in Figure 2 shows the implementation's most critical classes and how they relate to standard Java Swing classes.

Figure 2. High-level UML diagram of CloseAndMaxTabbedPane

Descriptions of the component's critical classes follow:

  1. CloseTabbedPaneUI (extends javax.swing.BasicTabbedPaneUI): This class is the new component's UI delegate object and is responsible for the tabbed pane's graphical aspects (including painting the Close and Maximize buttons). This class also fires events to the tabbed pane itself based on user actions and tries to be friendly with the application's current look and feel.
  2. CloseTabbedPaneEnhancedUI (extends CloseTabbedPaneUI): This class implements a different look and feel similar to the tabbed pane found in Eclipse 2.1. It overrides a set of paint methods to implement the enhanced look and feel.
  3. CloseAndMaxTabbedPane (extends javax.swing.JTabbedPane): This class extends the Java standard JTabbedPane and uses the CloseTabbedPaneUI. It also implements methods to add specific listeners (to Close and Maximize buttons) to the tabbed pane and interact with the UI.

Add buttons to the tabbed pane

It would be easy to add buttons if each tab were a separate Swing component. Ideally, then you would add a Close button by simply doing the following (forgetting layout aspects for the moment):

JButton closeButon = new JButton(closeButtonIcon); 
tab.add(closeButton);

And then adding an action listener by:

closeButton.addActionListener(someActionListener);

Unfortunately adding buttons is not that easy. Each tab in a tabbed pane is not a Swing component. Each tab's header (border, title, icon) is painted by the BasicTabbedPaneUI class. To paint our own version of the tabbed pane (i.e., to add the little buttons in the corner of each tab), we need to extend this class and override the paint methods to actually draw the buttons. Please note that the buttons are just painted graphics and not standard Java JButton components.

Interact with the pseudo buttons

Because there are no real buttons, we can't add action listeners to them. The only way to interact with these painted buttons is to add a mouse listener to the tabbed pane itself (or to one of its components). This mouse listener can then track mouse clicks or movements in the zone where the buttons are painted and fire specific events to the tabbed pane.

Create the extended UI class, CloseTabbedPaneUI

Before going further, we need some basic understanding of how the BasicTabbedPaneUI works. We focus only on the scroll-tab layout policy.

When you create a regular JTabbedPane, the default UI is an instance of the BasicTabbedPaneUI class. This instance has a reference to the tabbed pane itself, and it also sets the appropriate layout manager for the chosen tab layout policy. In the case of the scroll-tab layout, the layout manager creates a set of objects responsible for catching mouse actions from the user. We'll cover this in greater detail when we look at the implementation of mouse handlers. For now, just understand that the tabbed pane has some extra components when used with the scroll-tab layout policy.

So to implement our enhanced tabbed pane, we need to create a CloseTabPaneUI class that extends the BasicTabbedPaneUI class. We need to override the relevant paint methods from the base class and also add our mouse listeners.

Paint the pseudo buttons

To paint a tab, the UI uses the following method:

protected void paintTab(Graphics g, int tabPlacement, Rectangle[] rects, int tabIndex, Rectangle iconRect, Rectangle textRect)

where:

  • g is the tabbed pane's graphic context
  • tabPlacement is the tabs' positions in the tabbed pane (top, left, right, or bottom)
  • rects is an array of rectangles that bounds the painting area reserved for each tab
  • tabIndex is the index of a tab being painted
  • iconRect is the rectangle that bounds the painting area reserved for the tab icon
  • textRect is the rectangle that bounds the painting area reserved for the tab text

The layout of these rectangles is performed by the function layoutLabel() (we discuss more about this a bit later), which calls a SwingUtilities function, layoutCompoundLabel.

Basically, the BasicTabbedPaneUI paints something similar to the following:

And we want to change it to something like this:

To achieve this, we must resize the black rectangle, which is part of the rects array and bounds the tab area, and then add the two buttons on the right.

The resizing is straightforward, as each tab's width is provided by the method calculateTabWidth(). We just override this method using the following model:

protected int calculateTabWidth(int tabPlacement, int tabIndex, FontMetrics metrics) {
return super.calculateTabWidth(tabPlacement, tabIndex, metrics) + someExtraSpace;
}

Now we can override the paintTab() method itself and add our buttons:

protected void paintTab(Graphics g, int tabPlacement, Rectangle[] rects, int tabIndex, Rectangle iconRect, Rectangle textRect){
super.paintTab(...);
Rectangle tabRect = rects[tabIndex]; //the black rectangle
int selectedIndex = tabPane.getSelectedIndex();
boolean isSelected = selectedIndex == tabIndex;
boolean isOver = overTabIndex == tabIndex;
/* 
 * isOver returns true if the mouse is over the tab which is currently
 * painted.
 * overTabIndex returns the index of the tab the mouse is currently on.
 * overTabIndex is an instance of the class, and is modified
 * by the mouse listeners.
 */
// Like in Eclipse 2.1, the icons are painted only if the tab is
// selected or if the mouse is over the tab.
if (isOver || isSelected) {
// Calculate the coordinates where the buttons should be.
int dx = tabRect.x + tabRect.width - BUTTONSIZE - WIDTHDELTA;
int dy = (tabRect.y + tabRect.height) / 2 - 6;
//Paint the Close and Max buttons
   paintCloseIcon(g, dx, dy, isOver);
   paintMaxIcon(g, dx, dy, isOver);
}
}

Add the mouse listeners

New mouse listeners for CloseTabPaneUI are required to handle user clicks on the Close or Maximize buttons and provide a rollover effect when the mouse hovers over the buttons. We need a MouseListener for tracking user clicks and a MouseMotionListener for supporting rollover effects.

To understand this, we need to dig a bit deeper into the scroll-tab layout manager. The scroll-tab layout manager relies on an instance of class ScrollableTabSupport, tabScroller, which implements the tabbed pane's bar itself. It works similarly to a scroll pane. ScrollableTabSupport is an inner private class of the BasicTabbedPaneUI class. This class is composed of two main objects:

  1. tabPanel (not to be confused with the tabPane instance, which references the tabbed pane itself): An instance of ScrollableTabPanel (extends from JPanel) that contains the tab painting area and the arrow buttons to scroll the tabs.
  2. viewport: An instance of ScrollableTabViewport (extends from JViewport) and allows the display of only a certain number of tabs. For more information on how viewports work, please refer to Sun's Java documentation.

Both of the above classes are private inner classes as well. Also, the tabScroller instance is a private member of BasicTabbedPaneUI. Hence, it is not possible to access it from our extended CloseTabPaneUI class.

To circumvent this problem, we provided our own implementation of tabScroller for CloseTabPaneUI. Our implementation copied a lot of code from the base class version and features some additional logic that handles mouse events on the buttons. Please refer to the source code, which can be downloaded from Resources, for more details.

The mouse listener

BasicTabbedPaneUI implements a mouse listener, which processes mouse clicks on the tabs to change the selected tab. CloseTabPaneUI extends this mouse listener to provide additional functionality, as shown in the code below. We focus on the mouseReleased() method and the Close button to understand the logic. For the other methods and further details, please refer to the source code.

class MyMouseHandler extends MouseHandler {
   public MyMouseHandler() {
      super();
   }
   public void mouseReleased(MouseEvent e) {
      /* 
        * This function updates the overTabIndex variable
       * depending on the mouse position.
       */
      updateOverTab(e.getX(), e.getY());
      if (overTabIndex == -1) return;
      if (closeIndexStatus == PRESSED) {
         closeIndexStatus = OVER;
         tabScroller.tabPanel.repaint();
         ((CloseAndMaxTabbedPane)tabPane).fireCloseTabEvent(e,
                           overTabIndex);
         return;
      }
   }
}

closeIndexStatus is a member of the class CloseTabbedPaneUI, which represents the Close button's status of the tab the mouse is currently over. The possible values of closeIndexStatus are INACTIVE, OVER, or PRESSED. Be aware that the closeIndexStatus does not know to which tab it refers to, so closeIndexStatus must always be used with overTabIndex.

When the mouse is released over a tab, the method checks whether the user was previously pressing the Close button. If the status is true, then the status of the Close button is now OVER, as the mouse has been released. We then need to repaint the tab bar and notify the tabPane (i.e., the tabbed pane the UI is responsible for) by firing an event. Notice that the fireCloseTabEvent() method is specific to the CloseAndMaxTabbedPane, so we must cast the tabPane object beforehand appropriately.

The mouse motion listener

As opposed to the mouse listener, no standard mouse motion listener for BasicTabbedPaneUI is available. So we need to implement one from scratch:

class MyMouseMotionListener implements MouseMotionListener {
   public void mouseMoved(MouseEvent e) {
      mousePressed = false;
      setTabIcons(e.getX(), e.getY());
   }
   public void mouseDragged(MouseEvent e) {
      mousePressed = true;
      setTabIcons(e.getX(), e.getY());
   }
}

mousePressed is a boolean member of the CloseTabbedPaneUI, representing the mouse button's status, whether it is pressed or not. Some of the mouse listener's other methods use this variable.

The setTabIcons() method sets the status of both the Close and Maximize buttons according to the mouse position. For example, when the mouse is inside the Close button's zone, it sets the closeStatusIndex to OVER or PRESSED if the mouse is pressed.

Now, the question is which component should we add the listeners to? That is, which component tracks these mouse events? You might say the tabPane object, which is the instance of the tabbed pane itself. Well, that would be true if you use the wrap-tab layout policy. In the case of the scroll-tab layout policy, the component that matters is tabScroller.tabPanel.

To use the newly defined listener, you need to override the createMouseListener() method so it can return your own mouse listener:

protected MouseListener createMouseListener() {
   return new MyMouseHandler();
}

Then override the installListeners() method to add the mouse motion listener to the tabScroller.tabPanel object:

protected void installListeners() {
   //... Some code here
   if ((motionListener = new MyMouseMotionListener()) != null) {
      tabScroller.tabPanel.addMouseMotionListener(motionListener)
   }
}

And that's all you need to know about the CloseTabbedPaneUI class.

Create the extended JTabbedPane class, CloseAndMaxTabbedPane

Now that we have the UI, we need an enhanced tabbed pane that can handle the events fired by the UI, and the CloseAndMaxTabbedPane class is responsible for that task.

This class is fairly simple. Here is the constructor (simplified version):

public CloseAndMaxTabbedPane() {
   super.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
   paneUI = new CloseTabPaneUI();
   super.setUI(paneUI);
}

Methods to add listeners to a specific type of event follow. This one adds a listener to the close event:

public void addCloseListener(CloseListener l) {
   listenerList.add(CloseListener.class, l);
}

Next are methods to fire a specific type of event to the listeners:

public void fireCloseTabEvent(MouseEvent e, int overTabIndex) {
   this.overTabIndex = overTabIndex;
   EventListener cListeners[] = getListeners(CloseListener.class);
   for (int i = 0; i < cListeners.length; i++) {
      ((CloseListener) cListeners[i]).closeOperation(e);
   }
}

Use CloseAndMaxTabbedPane

First create a CloseAndMaxTabbedPane:

/* 
 * The true argument in the constructor means that the tabbed pane
 * is going to use an advanced interface. See the
 * CloseTabbedPaneEnhancedUI class for more details.
 */
CloseAndMaxTabbedPane tabbedPane = new CloseAndMaxTabbedPane(true)

Then add a close listener to the pane to close a tab when the user clicks on its Close button:

tabbedPane.addCloseListener(new CloseListener() {
   public void closeOperation(MouseEvent e) {
      tabbedPane.remove(tabbedPane.getOverTabIndex());
   }
});

A complete example application using the component is provided along with the source code in Resources.

Limitations

Currently, the CloseAndMaxTabbedPane implementation has the following limitations:

  1. The component only supports the scroll-tab layout policy. It does not support the other standard policy of wrap-tab layout.
  2. The component only supports TOP tab placement which is most commonly used. It does not support the other standard options LEFT, RIGHT, or BOTTOM.

Note on the design of BasicTabbedPaneUI class

While developing this component, we faced certain implementation issues because of the way the BasicTabbedPaneUI class is defined. An instance of LayoutManager is responsible for managing the JTabbedPane. The BasicTabbedPaneUI has a default layout, the wrap-tab layout, which is implemented by the class TabbedPaneLayout. This class is a protected class, nested into the BasicTabbedPaneUI class, which means it could be extended.

The scroll-tab layout is implemented by the class TabbedPaneScrollLayout, which is also nested in the BasicTabbedPaneUI class, and extends the class TabbedPaneLayout. Things are fine until this point. The difficulty is that this TabbedPaneScrollLayout class is private, which means extending this class or even modifying any object belonging to it is impossible. The only solution is to copy and paste the TabbedPaneScrollLayout directly from the BasicTabbedPaneUI class and modify it to suit your requirements.

Now, there could be important reasons why many of these inner classes are private. The class's design could be revamped, for example. The important fact is that when you try to clone a private class or method from this class into your code, you would see that this class or method makes calls to other private classes or methods, which you must clone as well! Ultimately, to make things work, you'll have to clone a lot of code you don't even care about.

Conclusion

The CloseAndMaxTabbedPane class overcomes several limitations of the standard Java JTabbedPane by adding the facility of closing and maximizing individual tabs. It also lets users selectively enable/disable any of these new buttons. Moreover, it provides a more professional look and feel similar to Eclipse 2.1. Not all user interfaces require these additional features of course, but those developers currently using the JTabbedPane class or planning a Java-based multiple-document interface environment should consider its usage.

We have used the CloseAndMaxTabbedPane class successfully in many of our own applications. We welcome any feedback regarding your own experiences with the class, either positive or negative, as well as comments pertaining to the design and layout.

David Bismut is a third-year student at the Ecole des Mines in Nantes (EMN), a French engineering school. He specializes in information management technologies. He was part of InStep, Infosys's global internship program in 2004. You can contact him at david.bismut@gmail.com. Krishnakumar Pooloth is a technical architect with Infosys Technologies. His areas of expertise include object design, component technology, Java, and expert systems. He holds a bachelor's degree in electronics and communication from Calicut University, India. You can contact him at krishnakumarp@infosys.com.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more