title: MigLayout and JTables within JScrollPanes within tabbed pages
body: <p>I submit the following implemented component in the spirit of giving back. The component satisfies my client's requirements, so I am not begging for help per se.  But the component provides interesting functionality that might be useful to others.</p>
  
  <p>Also, it raises some questions about apparently necessary hacks, and it's possible that my implementation is horrendously more complicated than necessary.  Any suggested alternative solutions could be useful to others as well.</p>
  
  <p>In a nutshell, a large table of data is split into multiple JTables, one in each page a multitabbed JTabPane.</p>
  
  <p>If you run the program and resize it in various ways, you'll see how the component is supposed to behave.  Explicitly, the requirements are:</p>
  
  <ol>
  <li><p>A tabular dataset with a varying number of records (1 to 500) is to be shown.</p></li>
  <li><p>A JTabbedPane contains one or more tabs, each containing only a JScrollPane.</p></li>
  <li><p>JScrollPanes can't have vertical scrollbars, may have horizontal scrollbars.</p></li>
  <li><p>Each JScrollPane contains a JTable.</p></li>
  <li><p>There are always just enough tabs in the JTabbedPane to collectively contain all the data rows in their JTables.</p></li>
  <li><p>The JTabbedPane size varies with the application JFrame size.</p></li>
  <li><p>The application may be freely resized by dragging with the mouse.</p></li>
  <li><p>At initialization, and whenever the application is resized, the JTabbedPane is rebuilt with just enough tab pages to hold all the records.  E.g. for 100 records, if at a particular panel size a tab's table can hold 8 records, then 13 tabs are created.</p></li>
  <li><p>If the the JTabbedPane's size is reduced so as to be too small to hold the number of tabs needed to hold all the data rows, nothing (or just a warning) is shown.</p></li>
  </ol>
  
  <p>A person with an academic interest in Java, MigLayout, and their interaction might want to consider four points:</p>
  
  <ol>
  <li><p>At a certain point in the source code is the following function call:</p>
  
  <p>tb = tabbedPane.getBoundsAt(0); </p></li>
  </ol>
  
  <p>It is a hack.  I cannot see what earthly need there is for this call; yet it (or something) is necessary.</p>
  
  <ol>
  <li><p>Theoretically, ScrollPane.getViewportBorderBounds() should give me the information to compute a tab page's table size, but I instead have to hack a value.  Am I wrong, or is it returning incorrect information where I am using it?</p></li>
  <li><p>There is a bewildering set of functions - paint(), repaint(), validate(), invalidate(), revalidate(), update().  I have found that particular functions need to be called at just the right time.  Order of calls is often, though not always obviously, very important.  This set of functions could really use rigorous but clear documentation of their interactions with AWT, Swing, and each other.  Their interaction with layout managers in general and MigLayout in particular could also use explanation.</p></li>
  <li><p>is there a solution to the requirements using generic Java that is a lot simpler than the approach I used?  Did I reinvent the wheel just to end up with a tractor tread?</p></li>
  </ol>
  
  <p>make:  javac -classpath  ScrollTableTest.java</p>
  
  <p>usage:  java -classpath  ScrollTableTest [total data rows]</p>
  
  <pre><code>import java.awt.*;
  import java.awt.event.*;
  import java.net.*;
  import java.util.*;
  import java.io.*;
  import java.text.*;
  import javax.swing.*;
  import javax.swing.event.*;
  import javax.swing.filechooser.*;
  import javax.swing.table.*;
  import javax.swing.table.AbstractTableModel;
  import javax.swing.table.TableModel;
  import javax.swing.table.TableColumn;
  import net.miginfocom.swing.MigLayout;
  
  public class ScrollTableTest
    extends    JFrame
  {
    public final static int APPWIDTH = 500; 
    public final static int APPHEIGHT = 300;
    public final static String[] CLIENT_COL_NAMES = { "Col 1", "Col 2", "Col 3", "Col 4" };
    public final static int COLS = CLIENT_COL_NAMES.length; 
    public final static int MAXTABS = 50; // arbitrary limit
    public final static int arbitraryTweek1 = 20;    
  
    String migDebugString = "";
  
    int[] dataRowsPerTabCount = new int [MAXTABS];
    JPanel topPane = null;
    DefaultTableModel clientsTableModel;
    String[][] clientData;
    JScrollPane scrollPane;
    Rectangle viewportBounds;
    JTable clientsTable;
    JTabbedPane tabbedPane;
    int dataRows, maxVisibleRow = -1;
    int rowsToShow = 1;
    int dataRowHeight;
  
    void printBasics()
    {
      if (scrollPane == null)
        return;
      System.out.println("");
      System.out.println("clientsTable height " + clientsTable.getHeight());
      System.out.println("topPane height:  " + topPane.getHeight());
      System.out.println("tabbedPane height " + tabbedPane.getHeight());
      System.out.println("scrollPane height:  " + scrollPane.getHeight());
      System.out.println("viewport bounds:  y " + viewportBounds.getY() +
            " height " + (int)viewportBounds.getHeight());
    }
    void printDims()
    {
      printBasics();
      double diff = viewportBounds.getHeight() - clientsTable.getHeight();
      System.out.println("dataRowHeight: " + dataRowHeight);
      System.out.println("differential:  " + diff);
    }
    void getGuiMetrics()
    {
      double diff;
      Rectangle tb;
      int clientRows = 20;
      int viewable = 0;
      int bottom;
      int computedSpHeight;
      int tabIx;
      boolean scrollbarHeightSet = false;
      int scrollbarHeight = 0;
      String title;
      topPane = new JPanel(new MigLayout("fill" + migDebugString, "[100%]", "[100%]"));
      setContentPane(topPane);
      validate();
  
      tabbedPane = new JTabbedPane();
      topPane.add(tabbedPane, "cell 0 0, grow");
  
      // create a temporary table of nominal size to use for table metrics
      clientData = new String[clientRows][COLS];
      clientsTableModel = new DefaultTableModel(clientData, CLIENT_COL_NAMES);
      clientsTable = new JTable(clientRows, COLS);
      clientsTable.setModel(clientsTableModel);
      clientsTable.setPreferredScrollableViewportSize(null);
      clientsTable.getTableHeader().setReorderingAllowed(false);
      clientsTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
      clientsTable.getSelectionModel().setSelectionInterval(0, 0);
  
      // created scroll pane containing table, and contained in tabbed pane
      scrollPane = new JScrollPane(clientsTable);
      scrollPane.setVerticalScrollBarPolicy(scrollPane.VERTICAL_SCROLLBAR_NEVER);
      scrollPane.setHorizontalScrollBarPolicy(scrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
      tabbedPane.setMaximumSize(new Dimension(topPane.getWidth(),  topPane.getHeight() - arbitraryTweek1));
  
      // For the entire allowed range of tabbed pages, calculate the area
      // within the tabbed pane available to hold a table.
      for (tabIx = 0; tabIx &lt; MAXTABS; ++tabIx)
      {
        JPanel panel = new JPanel(new MigLayout("fill" + migDebugString, "[100%]", "[100%]"));
        title = "Page " + (tabIx +1);
        tabbedPane.addTab(title, panel);
        panel.add(scrollPane, "cell 0 0, grow");
        if (tabIx == 0)
        {
          validate();
          dataRowHeight = clientsTable.getHeight() / clientRows;
        }
        else
          tabbedPane.revalidate();
  
        // we need to know how high the hz scrollbar is 
        if (!scrollbarHeightSet)
        {
          JScrollBar hzScrollBar = scrollPane.getHorizontalScrollBar();
          if (hzScrollBar != null)
            scrollbarHeight = hzScrollBar.getHeight();
          else
            scrollbarHeight = 0;
          scrollbarHeightSet = true;
        }
        // pick one
        boolean useViewport = false;
        boolean compViewport = false;
        boolean compViewport2 = true; // this one works best.
  
        // this presumptively correct method barely works
        if (useViewport)
        {
          viewportBounds = scrollPane.getViewportBorderBounds();
          viewable = ((int)viewportBounds.getHeight()) / dataRowHeight;
        }
        // this hack works better
        if (compViewport) 
        {
          tb = tabbedPane.getBoundsAt(0);
          bottom = (int)(tb.getY() + tb.getHeight());
          computedSpHeight = tabbedPane.getHeight() - (dataRowHeight + bottom);
          viewable = (computedSpHeight - scrollbarHeight) / dataRowHeight;
        }
        // this works well.  But what does JTabbedPane.getBoundsAt() have to do with it?
        if (compViewport2)
        {
          tb = tabbedPane.getBoundsAt(0); // !!! Worse Than Failure - this must be here!
          viewable = (scrollPane.getHeight() - scrollbarHeight) / dataRowHeight;
        }
        if (viewable &gt; 0)
          viewable -= 1; // take out the title row
  
        dataRowsPerTabCount[tabIx] = viewable;
      }      
    } // getGuiMetrics
  
    void updateTable()
    {
      int tabIx, numTabs, rowsPerTab = 0, maxDisplayableRows = 0, rowsAdded, rowsThisTime;
      boolean accepted = false;
  
      getGuiMetrics();
  
      topPane = new JPanel(new MigLayout("fill" + migDebugString, "[100%]", "[100%]"));
      setContentPane(topPane);
  
      // how many tabs are needed to display all the data rows?
      for (tabIx = 0; !accepted &amp;&amp; tabIx &lt; MAXTABS; ++tabIx)
      {
        rowsPerTab = dataRowsPerTabCount[tabIx];
        maxDisplayableRows = rowsPerTab * (tabIx +1);
        if (maxDisplayableRows &gt;= dataRows)
        {
          accepted = true;
          numTabs = tabIx +1;
        }
      }
      // did we find a best fit solution?
      if (!accepted)
      {
        topPane.add(new JLabel("Not enough space for all data rows"));
        return;
      }
      tabbedPane = new JTabbedPane();
      validate();
      tabbedPane.setMaximumSize(new Dimension(topPane.getWidth(),  topPane.getHeight() - arbitraryTweek1));
      topPane.add(tabbedPane, "cell 0 0, grow");
  
      // create and fill the tab pages
      for (tabIx = 0, rowsAdded = 0; rowsAdded &lt; dataRows; ++tabIx)
      {
        if (rowsAdded + rowsPerTab &gt; dataRows)
          rowsThisTime = dataRows - rowsAdded;
        else
          rowsThisTime = rowsPerTab;
  
        // create the table for the page
        clientData = new String[rowsThisTime][COLS];
        clientsTableModel = new DefaultTableModel(clientData, CLIENT_COL_NAMES);
        clientsTable = new JTable(rowsThisTime, COLS);
        clientsTable.setModel(clientsTableModel);
        clientsTable.setPreferredScrollableViewportSize(null);
        clientsTable.getTableHeader().setReorderingAllowed(false);
        clientsTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        clientsTable.getSelectionModel().setSelectionInterval(0, 0);
  
        // fill the table with test data
        for (int row = 0; row &lt; rowsThisTime; ++row)
        {
          for (int col = 0; col &lt; COLS; ++col)
          {
            String cellVal = "tab " + (tabIx +1) + " cell row " + (row+1) + " col " + (col+1);
            clientsTableModel.setValueAt(cellVal, row, col);
          }
        }
        // create scroll pane holding table
        scrollPane = new JScrollPane(clientsTable);
        scrollPane.setVerticalScrollBarPolicy(scrollPane.VERTICAL_SCROLLBAR_NEVER);
        scrollPane.setHorizontalScrollBarPolicy(scrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
  
        // create tab panel holding the scroll pane
        JPanel panel = new JPanel(new MigLayout("fill" + migDebugString, "[100%]", "[100%]"));
        String title = "Page " + (tabIx +1);
        tabbedPane.addTab(title, panel);
        panel.add(scrollPane, "cell 0 0, grow");
  
        rowsAdded += rowsPerTab;
      }
      tabbedPane.revalidate();
    } // updateTable
  
    void init(String[] args)
    {
      //  uncomment this to see the migLayout component border highlighting
      //  migDebugString = ", debug";
  
      // total of how many data rows?
      if (args.length &lt; 1)
      {
        dataRows = 20;
      } 
      else
      {
        dataRows = Integer.valueOf(args[0]);
        if (dataRows &lt;= 0)
        {
          System.out.println("bad arg");
          System.exit(0);
        } 
      }
      setSize(APPWIDTH, APPHEIGHT);
      addComponentListener(new ComponentAdapter()
      {
        public void componentShown(ComponentEvent evt)
        {
        }
        public void componentHidden(ComponentEvent evt)
        {
        }
        public void componentResized(ComponentEvent evt)
        {
          updateTable();
        } // componentResized()
      }); // addComponentListener
  
      topPane = new JPanel(new MigLayout("fill" + migDebugString, "[100%]", "[100%]"));
      setContentPane(topPane);
  
      // center app window
      GraphicsConfiguration gc = getGraphicsConfiguration();
      Rectangle bounds = gc.getBounds();
      setLocation((int)((bounds.width-APPWIDTH) /2),
                             (int)((bounds.height - APPHEIGHT) /2));
      setVisible(true);
    }
    public static void main(String[] args)
    {
      try
      {
        ScrollTableTest thisTest = new ScrollTableTest();
        thisTest.init(args);
      }
      catch (Exception e)
      {
        System.out.println("runTest caught exception:  " + e.getMessage());
        e.printStackTrace();
      }
    }
  } // class test
  </code></pre>