![]() |
| ||
|
SummaryBy David Bismut, Krishnakumar Pooloth
In this article, David Bismut and Krishnakumar Pooloth presentCloseAndMaxTabbedPane, an enhanced tabbed pane with Close and Maximize buttons and a look and feel similar to Eclipse 2.1. The authors discuss the component's design and implementation aspects. They also explain the design of theBasicTabbedPaneUIclass, which is an important element in the implementation. (2,000 words; September 6, 2004)
![]()
rofessional 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:
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.
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.
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 textThe 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:
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.
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:
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.
About the author
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.
![]() |
|
|
Copyright © 2004 JavaWorld.com, an IDG company |
|