Click here to Skip to main content
15,886,199 members
Articles / Desktop Programming / Swing
Tip/Trick

Enhanced JTabbedPane

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
2 Sep 2014CPOL1 min read 14.5K   2   4
This tip provides code for an enhanced version of the JTabbedPane which allows the developer to add toolbar buttons to the beginning or end of the tab set.

Introduction

I have decided that in the interest of modern design and encouraging a certain workflow, a project on which I am working would be best presented with a minimalist design consisting of only tabs in the main frame. However, after much searching, I was only able to find discouraging advice on the ability to add toolbar buttons to the tab set. The key is that these buttons should be just buttons. It's easy enough to add a 16x16 image as a tab header, but the desired effect is not for the Save button, for example, to have content associated with it. Instead, the Save button should just be there waiting to be clicked, and always be visible regardless of whether the array of tabs has scrolled. These classes provide the ability to add one or more buttons to either the leading or trailing side (Locale dependent) of the tab set.

Using the Code

The main class here is ETabbedPane which derives from JPanel. It contains a JTabbedPane which fills the widget and two toolbars, one each at the northwest and northeast corner. To interact with the tabs, fetch them using getJTabbedPane() and interact with them as you normally would. Rather than referring to the toolbars as left and right, they are referenced as 'leading' and 'trailing' to facilitate internationalization.

Here is the source:

Java
import java.awt.ComponentOrientation;
import java.awt.FlowLayout;
import java.awt.Insets;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;

import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.JToolBar;
import javax.swing.SpringLayout;

public class ETabbedPane extends JPanel {
    public enum ButtonSide {
        LEADING,
        TRAILING;
    }
    /**
     *
     */
    private static final long serialVersionUID = -2090869893562246860L;
    private JTabbedPane tabs;
    private JToolBar leadingButtons;
    private JToolBar trailingButtons;
    //private JPanel dropDown;
    private OffsetTabbedPaneUI tabUI;
    
    public ETabbedPane(ComponentOrientation orient) {
        SpringLayout sl = new SpringLayout();
        this.setLayout(sl);
        
        this.leadingButtons = new JToolBar();
        this.leadingButtons.setBorderPainted(false);
        this.leadingButtons.setFloatable(false);
        this.leadingButtons.setOpaque(false);
        this.leadingButtons.setComponentOrientation(orient);
        this.leadingButtons.setLayout(new FlowLayout(FlowLayout.LEADING, 1, 1));
        sl.putConstraint(SpringLayout.NORTH, this.leadingButtons, 0, SpringLayout.NORTH, this);
        String side = ((orient == ComponentOrientation.RIGHT_TO_LEFT) ? SpringLayout.EAST : SpringLayout.WEST);
        sl.putConstraint(side, this.leadingButtons, 0, side, this);
        this.add(leadingButtons);
        
        this.trailingButtons = new JToolBar();
        this.trailingButtons.setBorderPainted(false);
        this.trailingButtons.setFloatable(false);
        this.trailingButtons.setOpaque(false);
        this.trailingButtons.setComponentOrientation(orient);
        this.trailingButtons.setLayout(new FlowLayout(FlowLayout.LEADING, 1, 1));
        sl.putConstraint(SpringLayout.NORTH, this.trailingButtons, 0, SpringLayout.NORTH, this);
        side = ((orient == ComponentOrientation.RIGHT_TO_LEFT) ? SpringLayout.WEST : SpringLayout.EAST);
        sl.putConstraint(side, this.trailingButtons, 0, side, this);
        this.add(trailingButtons);
        
        this.tabs = new JTabbedPane();
        this.tabs.setComponentOrientation(orient);
        sl.putConstraint(SpringLayout.NORTH, this.tabs, 0, SpringLayout.NORTH, this);
        sl.putConstraint(SpringLayout.SOUTH, this.tabs, 0, SpringLayout.SOUTH, this);
        sl.putConstraint(SpringLayout.WEST, this.tabs, 0, SpringLayout.WEST, this);
        sl.putConstraint(SpringLayout.EAST, this.tabs, 0, SpringLayout.EAST, this);
        tabUI = new OffsetTabbedPaneUI();
        this.tabs.setUI(tabUI);
        this.add(tabs);
        
        this.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
                if (tabs.getTabCount() > 0) {
                    tabs.getSelectedComponent().requestFocusInWindow();
                }
            }
        });
    }

    /**
     * @return the tabs
     */
    public JTabbedPane getJTabbedPane() {
        return tabs;
    }
    
    public void addButton(JButton button, ButtonSide side) {
        button.setBorderPainted(false);
        button.setFocusable(false);
        button.setMargin(new Insets(1, 1, 1, 1));
        ((side == ButtonSide.LEADING) ? this.leadingButtons : this.trailingButtons).add(button);
        this.tabUI.setMinHeight(
                Math.max(this.leadingButtons.getPreferredSize().height,
                         this.trailingButtons.getPreferredSize().height));
        this.tabUI.setLeadingOffset(this.leadingButtons.getPreferredSize().width);
        this.tabUI.setTrailingOffset(this.trailingButtons.getPreferredSize().width);
        this.validate();
    }
}

In order for the widget to work as intended, you need an additional class which overrides the default L&F. Source:

Java
import java.awt.Insets;

import javax.swing.plaf.basic.BasicTabbedPaneUI;

public class OffsetTabbedPaneUI extends BasicTabbedPaneUI {
    private int leadingOffset = 0;
    private int minHeight = 0;
    private int trailingOffset;
    
    public OffsetTabbedPaneUI() {
        super();
    }

    /* (non-Javadoc)
     * @see javax.swing.plaf.basic.BasicTabbedPaneUI#calculateTabHeight(int, int, int)
     */
    @Override
    protected int calculateTabHeight(int tabPlacement, int tabIndex,
            int fontHeight) {
        return Math.max(super.calculateTabHeight(tabPlacement, tabIndex, fontHeight), this.minHeight);
    }

    /* (non-Javadoc)
     * @see javax.swing.plaf.basic.BasicTabbedPaneUI#getTabAreaInsets(int)
     */
    @Override
    protected Insets getTabAreaInsets(int tabPlacement) {
        // ignores tab placement for now
        return new Insets(0, this.leadingOffset, 0, this.trailingOffset);
    }

    /**
     * @param offset the offset to set
     */
    public void setLeadingOffset(int offset) {
        this.leadingOffset = offset;
    }

    /**
     * @param minHeight the minHeight to set
     */
    public void setMinHeight(int minHeight) {
        this.minHeight = minHeight;
    }

    public void setTrailingOffset(int offset) {
        this.trailingOffset = offset;
    }
}

Here is an example application demonstrating several tabs and buttons on the leading and trailing ends (running under Kubuntu):

Image 1

History

  • 2014-09-03 0603 CDT: Added screenshot
  • Previously: This is the first version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Engineer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionThis does not work using JTabbedPane.SCROLL_TAB_LAYOUT Pin
Steven197313-Sep-16 5:55
Steven197313-Sep-16 5:55 
AnswerRe: This does not work using JTabbedPane.SCROLL_TAB_LAYOUT Pin
kjburns198019-Oct-16 18:56
kjburns198019-Oct-16 18:56 
I'm sorry it took so long to get back to you. I don't log on here very often, so I didn't know your question was here.

Thank you for pointing out this bug. I'm interested in finding a fix for it, because it also affects the project that I'm working on. I played around with it and upon closer inspection saw that not only are the buttons drawing on top of the scroll arrows, but they are also drawing on top of the last tab when they get that far to the right.

I spent several hours today trying potential fixes without success. I looked through the source for JTabbedPane on grepcode to see how the layout process works, hoping for some insight.

I found that there is a JViewport that is used when SCROLL_TAB_LAYOUT is active. Essentially, the source lays out all tabs in a virtual horizontal row and shifts the row under the viewport to display the appropriate tabs according to user scrolling. That JViewport appears to be a direct child component of the JTabbedPane instance.

In the java-1.8.0-openjdk source for BasicTabbedPaneUI, lines 3026 to 3061, the bounding Rectangle for this viewport is calculated. However, it does not take into account the result from OffsetTabbedPaneUI::getTabAreaInsets().right, which is a mystery to me because somehow the value for OffsetTabbedPaneUI::getTabAreaInsets().left is taken into account somewhere along the way, because the tabs are offset properly when buttons are added in the LEADING position. The key is being able to modify that bounding Rectangle as is done on line 3060. Seems easy enough, except:
(a) it has to be modified any time a button is added/removed, or when the bounding container is resized, and
(b) it's easy enough to loop through all the components of ETabbedPane's underlying JTabbedPane, and we can check for whether each component is a JViewport, but what if the component on a tab is also a JViewport?
That second point is quite problematic to me because I don't know enough about reflection to be able to determine whether I have the right viewport.

Once the tabs' viewport is identified, it seems simple enough to set the width of its bounding rectangle to (width of ETabbedPane) - (width of ETabbedPane::leadingButtons) - (width of ETabbedPane::trailingButtons).

I hope that gives you enough to go on, and if you figure it out from here, I hope you will post the fix here.
QuestionA screenshot of finished thing may have been nice Pin
Sacha Barber3-Sep-14 0:20
Sacha Barber3-Sep-14 0:20 
AnswerRe: A screenshot of finished thing may have been nice Pin
kjburns19803-Sep-14 1:39
kjburns19803-Sep-14 1:39 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.