4

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 {}
}
splungebob
  • 5,357
  • 2
  • 22
  • 45
  • 1. JTabbedPane is really spoiled CardLayout, no suprices then with protected methods for BasicsXxxUI (excluding WindowsXxxL&F), 2. have to kill [BasicTabbedPaneUI (methods at bottom, in your case without super.xxxXxx???)](http://stackoverflow.com/a/7056093/714968) – mKorbel Jul 04 '14 at 10:35
  • Man, this problem has been killing me for days. Were you able to find a decent solution? – LppEdd Oct 19 '20 at 19:29

1 Answers1

2

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.

Ordous
  • 3,844
  • 15
  • 25
  • +1 Good info. So far (after reading your edit), I haven't had an issue with the solution in the OP, perhaps because I only have a small, fixed amount of tabs that can be created. I look more into it next week. Thanks. – splungebob Jul 03 '14 at 19:39
  • @splungebob I had an issue where I opened 6 tabs and then made the window small enough so that the tabs wouldn't fit horizontally. It then displayed only the last 3 tabs – Ordous Jul 03 '14 at 19:56
  • @splungebob I'm sure that here were "How to simulating/painting JTabbedPane without using JTabbedPane" a few times, or at Coreranch (by Walter Laan, Michael Dunn, aterai) – mKorbel Jul 04 '14 at 10:39
  • @Ordous +1 for touching an issue, disagree with setXxxSize out of BasicsTabbedPaneUI, probably platform and L&F sensitive, can has issue with Nimbus and WindowsClassicsL&F – mKorbel Jul 04 '14 at 10:41
  • @mKorbel Indeed, as I put in the edit - setXxxSize is not a good way to solve this. Essentially this is a problem that the basic UI/L&F allows multiple tab rows - hence tab panel should not be wider than children. Given you don't know the width you'll be allocated, I can't see a way to not reserve several rows. A good solution would be scrollable tabs like the ones in Firefox (single row, but you can scroll tabs right and left), but I do not know a L&F that does it. – Ordous Jul 04 '14 at 11:04
  • @Ordous `be scrollable tabs like the ones in Firefox` - JList with XxxRenderer contians custom painting or custom JToggleButtons – mKorbel Jul 04 '14 at 11:10
  • @mKorbel So that would pretty much be "Construct your own tabbed pane out of simpler components", since you've got a list with a custom renderer for the tabs and a separate pane with (supposedly) card layout as contents. – Ordous Jul 04 '14 at 11:16