splungebob
splungebob

Reputation: 5415

Empty JTabbedPane

I'm having an issue creating an empty JTabbedPane where the only portion to be seen on the GUI are the row of tabs.

Everytime I add a new tab with an "empty" component, the height of the JTabbedPane increases, but why?

The current workaround is to override getPreferredSize(), but it seems kludgy to me. Comment out the overridden method to see what I mean.

Am I missing something obvious?


Background:

We need a JTabbedPane where the tabbed pane starts off with 2 tabs, but the user can add more tabs as needed, up to 10. In addition, each tab contains the same components, but with different data. The decision was made to fake the look of a JTabbedPane, by implementing an empty JTabbedPane solely for the look, and to use a single fixed JPanel whose contents will be refreshed based on the tab clicked.

(Normally, I could just recreate the JPanel n-times, but that would nightmarish for the presenter classes who control the UI, which is beyond the scope of my question.)


import java.awt.*;
import java.awt.event.*;

import javax.swing.*;

public class CustomTabbedPane implements Runnable
{
  static final int MAX_TABS = 11; // includes the "add" tab

  JPanel pnlTabs;
  JTabbedPane tabbedPane;

  public static void main(String[] args)
  {
    SwingUtilities.invokeLater(new CustomTabbedPane());
  }

  public void run()
  {
    JPanel p = buildPanel();
    JFrame frame = new JFrame();
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setContentPane(p);
    frame.setSize(800,400);
    frame.setVisible(true);
  }

  private JPanel buildPanel()
  {
    tabbedPane = new JTabbedPane()
    {
      @Override
      public Dimension getPreferredSize()
      {
        Dimension dim = super.getPreferredSize();
        dim.height = getUI().getTabBounds(this, 0).height + 1;
        return dim;
      }
    };

    tabbedPane.addTab("Tab 1", getEmptyComp());
    tabbedPane.addTab("Tab 2", getEmptyComp());
    tabbedPane.addTab("+", new TabCreator());

    tabbedPane.addMouseListener(new MouseAdapter()
    {
      @Override
      public void mouseClicked(MouseEvent e)
      {
        addTab();
      }
    });

    JScrollPane scroll = new JScrollPane(new JTable(5,10));

    JPanel p = new JPanel(new BorderLayout());
    p.add(tabbedPane, BorderLayout.NORTH);
    p.add(scroll,  BorderLayout.CENTER);
    p.setBorder(BorderFactory.createLineBorder(Color.BLUE.darker(), 1));

    return p;
  }

  private void addTab()
  {
    if (tabbedPane.getSelectedComponent() instanceof TabCreator)
    {
      int selIndex = tabbedPane.getSelectedIndex();

      if (tabbedPane.getComponentCount() < MAX_TABS)
      {
        if (selIndex == tabbedPane.getComponentCount()-1)
        {
          String title = "Tab " + (selIndex + 1);
          tabbedPane.insertTab(title, null, getEmptyComp(), "", selIndex);
          tabbedPane.setSelectedIndex(selIndex);

          if (tabbedPane.getComponentCount() == MAX_TABS)
          {
            tabbedPane.setEnabledAt(MAX_TABS-1, false);
          }
        }
      }
    }
  }

  private Component getEmptyComp()
  {
    return Box.createVerticalStrut(1);
  }

  class TabCreator extends JLabel {}
}

Upvotes: 4

Views: 1426

Answers (1)

Ordous
Ordous

Reputation: 3884

Great question! But it's fairly straightforward to get a hint on what's happening.

The problem is that your content does not have a minimum width, preferred size is not set, tab placement is top/bottom and the UI is default.

Since preferred size is not set, then when the layout is revalidated the calculations of space required go into the BasicTabbedPaneUI method Dimension calculateSize(false).

That reads:

int height = 0;
int width = 0;
<other vars>
// Determine minimum size required to display largest
// child in each dimension
<actual method>

Here it calculates the minimum size to accommodate any child and stores it into height/width. In your case this yields something like 10,10 (because of the single Label tab creator I think, I didn't follow that one).

Then happens the magic:

switch(tabPlacement) {
    case LEFT:
    case RIGHT:
        height = Math.max(height, calculateMaxTabHeight(tabPlacement));
        tabExtent = preferredTabAreaWidth(tabPlacement, height - tabAreaInsets.top - tabAreaInsets.bottom);
        width += tabExtent;
        break;
    case TOP:
    case BOTTOM:
    default:
        width = Math.max(width, calculateMaxTabWidth(tabPlacement));
        tabExtent = preferredTabAreaHeight(tabPlacement, width - tabAreaInsets.left - tabAreaInsets.right);
        height += tabExtent;
}

What happens here is it sets the preferred width to be the maximum of the largest tab width and the largest child width. In your case it's around 44 for the tab text. The tabExtent is then calculated to see just how many rows of tabs are needed to support this preferred width. In your case - it's 1 extra row of tabs for each tab. That's where the extra height in preferredSize().height comes from. Essentially because for horizontal tab placement it cares about width first, then height.

How to fix:

  1. Set a preferred size :) I know a lot of people say don't set the preferred size, but in this case this will just work. Since a preferred size is set (via actually setting it, not overriding getPreferredSize()), the code will never get to counting tabs.
  2. Give at least one of your children a size (via setPreferredSize or overriding getPreferredSize). If one of the childrens width is that of the frame, or, say, the table at the bottom the TabbedPane will not be allocating an extra row for each tab, since a single row will fit everything.
  3. Make your own UI for the tabbed pane. It may be easier to make your own tabbed pane though really, I've never done this.

EDIT:

After thinking about this a bit more, I realized that solution number 1 AND your own solution suffer from the flaw that, if the tabbed pane actually does require multiple rows for the tabs (hello frame resizes), bad things will happen. Don't use it.

Upvotes: 2

Related Questions