United StatesChange Country, Oracle Worldwide Web Sites Communities I am a... I want to...
JDK-6368047 : jtabbedpane does not fire state changed when inner tabs are removed

Details
Type:
Bug
Submit Date:
2006-01-02
Status:
Resolved
Updated Date:
2011-02-16
Project Name:
JDK
Resolved Date:
2006-04-26
Component:
client-libs
OS:
generic,windows_xp
Sub-Component:
javax.swing
CPU:
x86,generic
Priority:
P3
Resolution:
Fixed
Affected Versions:
5.0,6
Fixed Versions:

Related Reports
Relates:
Relates:

Sub Tasks

Description
FULL PRODUCT VERSION :

java version "1.5.0_05"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_05-b05)
Java HotSpot(TM) Client VM (build 1.5.0_05-b05, mixed mode)

ADDITIONAL OS VERSION INFORMATION :
Windows XP Version 5.1 (Build 2600.xpsp_sp2_gdr.050301-1519: Service Pack 2)

A DESCRIPTION OF THE PROBLEM :
when a tabbed pane is displayed and you delete tabs programatically ( say via a button ) , deleting outer  tabs generate state changed but deleting inner tabs do not.

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Please find a Abbot base test fixture source code in this bug report.
You will need the Abbot jars in your class path.

http://abbot.sourceforge.net/doc/download.shtml



EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
  state changed must be fired when inner tabs are deleted
ACTUAL -
  state changed is fired when outter tabs are deleted

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
package com.tabbedpane.test;
//
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.InvocationTargetException;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import junit.extensions.abbot.ComponentTestFixture;
import abbot.tester.ComponentTester;
import abbot.tester.JTabbedPaneLocation;
import abbot.tester.JTabbedPaneTester;
@SuppressWarnings("all")
public class JTabbedPaneCompTestCase extends ComponentTestFixture
{
  private JTabbedPaneTester tester;
  private JFrame f;
  private JTabbedPane tab;
  private JPanel firstPnl, secondPnl, thirdPnl, fourthPnl, controlPnl;
  private JButton removeSecondBtn, removeFourthBtn;
  private boolean fired;
  //
  protected void setUp()
  {
    try
    {
      EventQueue.invokeAndWait(new Runnable()
      {
        public void run()
        {
          firstPnl = new JPanel();
          secondPnl = new JPanel();
          thirdPnl = new JPanel();
          fourthPnl = new JPanel();
          controlPnl = new JPanel();
          //
          removeSecondBtn = new JButton("removeSecond");
          removeSecondBtn.addActionListener(new ActionListener()
          {
            public void actionPerformed(ActionEvent pE)
            {
              tab.remove(secondPnl);
            }
          });
          removeFourthBtn = new JButton("removeFourth");
          removeFourthBtn.addActionListener(new ActionListener()
          {
            public void actionPerformed(ActionEvent pE)
            {
              tab.remove(fourthPnl);
            }
          });
          controlPnl.add(removeSecondBtn);
          controlPnl.add(removeFourthBtn);
          //
          tab = new JTabbedPane();
          tab.add(firstPnl, "firstPnl");
          tab.add(secondPnl, "secondPnl");
          tab.add(thirdPnl, "thirdPnl");
          tab.add(fourthPnl, "fourthPnl");
          tab.addChangeListener(new ChangeListener()
          {
            public void stateChanged(ChangeEvent pE)
            {
              fired = true;
            }
          });
          //
          f = new JFrame("JTabbedPaneTestCase");
          f.add(tab, BorderLayout.CENTER);
          f.add(controlPnl, BorderLayout.SOUTH);
          f.setSize(new Dimension(300, 400));
          f.setVisible(true);
          centerOnScreen(f);
          //
          fired = false;
        }
      });
    }
    catch (InvocationTargetException ex)
    {
      ex.printStackTrace();
    }
    catch (InterruptedException ex)
    {
      ex.printStackTrace();
    }
    tester = (JTabbedPaneTester) ComponentTester.getTester(JTabbedPane.class);
  }
  //
  public final void testRemoveFourthPnl()
  {
    tester.actionDelay(2000);
    tester.actionSelectTab(tab, new JTabbedPaneLocation(3));
    assertTrue("State Changed On Selection:", fired);
    fired = false;
    tester.actionDelay(2000);
    tester.actionClick(removeFourthBtn);
    tester.actionDelay(2000);
    assertTrue("State Changed On Removal:", fired);
  }
  //
  public final void testRemoveSecondPnl()
  {
    tester.actionDelay(2000);
    tester.actionSelectTab(tab, new JTabbedPaneLocation(1));
    assertTrue("State Changed On Selection:", fired);
    fired = false;
    tester.actionDelay(2000);
    tester.actionClick(removeSecondBtn);
    tester.actionDelay(2000);
    assertTrue("State Changed On Removal:", fired);
  }
  //
  @Override
  protected void tearDown() throws Exception
  {
    f.setVisible(false);
    f = null;
  }
  //
  public JTabbedPaneCompTestCase(String name)
  {
    super(name);
  }
  //
  public static void main(String[] args)
  {
    junit.extensions.abbot.TestHelper.runTests(args, JTabbedPaneCompTestCase.class);
  }
  //
  private static final void centerOnScreen(Component pComponent)
  {
    Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
    Dimension componentSize = pComponent.getSize();
    if (componentSize.height > screenSize.height)
    {
      componentSize.height = screenSize.height;
    }
    if (componentSize.width > screenSize.width)
    {
      componentSize.width = screenSize.width;
    }
    pComponent.setLocation((screenSize.width - componentSize.width) / 2,
        (screenSize.height - componentSize.height) / 2);
  }
}
---------- END SOURCE ----------
Contribution by java.net member leouser:

A DESCRIPTION OF THE FIX :
BUGID: 6368047 jtabbedpane does not fire state changed when inner tabs are removed
FILES AFFECTED: javax.swing.JTabbedPane.
JDK VERSION
jdk-6-rc-bin-b64-linux-i586-15_dec_2005.bin

Discusion(embeded in test case):
/**
 * BUGID: 6368047 jtabbedpane does not fire state changed when inner tabs are removed.
 * Ive ran into this bug in real life and I don't like it!  The problem is 2 fold
 * 1rst in removeTabAt it only selects if were at the end index.
 * 2nd in the DefaultSingleSelectionModel, we only fire a ChangeEvent if
 * there is a change numerically.  It doesn't matter if its different tab
 * or not.  To remedy this I have:
 * made sure in removeTabAt always selects somethings.  It also
 * doesn't matter what remove is called, they all appear to route
 * to removeTabAt which is where the selection happens.
 * Subclassed DefaultSingleSelectionModel to fire in most cases a ChangeEvent
 * if there was a selection, regardless of selected index.  I chose subclassing
 * over writing a new version because it saves alot of code duplication and
 * there aren't too many gymnastics involved with making it fire, just a flag.
 *
 * Being able to know if a different Tab has been selected after removal is
 * essential to the user.  Ive had to play games with the JTabbedPane because
 * of this behavior.  This 'correct' behavior would eliminate alot of the
 * shenanigans Ive had to write.
 *
 * TESTING STRATEGY:
 * Hit the remove button and it will remove the current tab.  Watch for output
 * on the console.  A different hashcode should come up as well as the correct
 * tab name for the newly selected tab.  To show the change execute this against
 * an unmodified JTabbedPane and see the results.
 *
 * FILES AFFECTED: javax.swing.JTabbedPane.
 * JDK VERSION
 * jdk-6-rc-bin-b64-linux-i586-15_dec_2005.bin
 *
 * test ran succesfully on a SUSE 7.3 Linux distribution
 *
 * Brian Harry
 * ###@###.###
 * Jan 20, 2006
 */

UNIFIED DIFF:
--- /home/nstuff/java6/jdk1.6.0/javax/swing/JTabbedPane.java	Thu Dec 15 02:17:37 2005
+++ /home/javarefs/javax/swing/JTabbedPane.java	Fri Jan 20 17:05:13 2006
@@ -176,7 +176,7 @@
         setTabPlacement(tabPlacement);
         setTabLayoutPolicy(tabLayoutPolicy);
         pages = new Vector(1);
-        setModel(new DefaultSingleSelectionModel());
+        setModel(new AlwaysFiringSingleSelectionModel());
         updateUI();
     }
 
@@ -859,6 +859,9 @@
             }
         }
 
+        if (selected < (tabCount - 1)) {
+            setSelectedIndexImpl(getSelectedIndex());
+        }
         revalidate();
         repaint();
     }
@@ -2213,4 +2216,25 @@
         }
         return -1;
     }
+
+    @SuppressWarnings("serial")
+    private static class AlwaysFiringSingleSelectionModel extends DefaultSingleSelectionModel{
+	
+        boolean fired;
+        @Override
+        public void setSelectedIndex(int index){
+            fired = false;
+            super.setSelectedIndex(index);
+            if(!fired && index != -1)
+                fireStateChanged();
+        }
+
+        @Override
+        public void fireStateChanged(){
+            fired = true;
+            super.fireStateChanged();
+        }
+
+    }
+
 }


JUnit TESTCASE :
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.event.*;
import static java.lang.System.out;

/**
 * BUGID: 6368047 jtabbedpane does not fire state changed when inner tabs are removed.
 * Ive ran into this bug in real life and I don't like it!  The problem is 2 fold
 * 1rst in removeTabAt it only selects if were at the end index.
 * 2nd in the DefaultSingleSelectionModel, we only fire a ChangeEvent if
 * there is a change numerically.  It doesn't matter if its different tab
 * or not.  To remedy this I have:
 * made sure in removeTabAt always selects somethings.  It also
 * doesn't matter what remove is called, they all appear to route
 * to removeTabAt which is where the selection happens.
 * Subclassed DefaultSingleSelectionModel to fire in most cases a ChangeEvent
 * if there was a selection, regardless of selected index.  I chose subclassing
 * over writing a new version because it saves alot of code duplication and
 * there aren't too many gymnastics involved with making it fire, just a flag.
 *
 * Being able to know if a different Tab has been selected after removal is
 * essential to the user.  Ive had to play games with the JTabbedPane because
 * of this behavior.  This 'correct' behavior would eliminate alot of the
 * shenanigans Ive had to write.
 *
 * TESTING STRATEGY:
 * Hit the remove button and it will remove the current tab.  Watch for output
 * on the console.  A different hashcode should come up as well as the correct
 * tab name for the newly selected tab.  To show the change execute this against
 * an unmodified JTabbedPane and see the results.
 *
 * FILES AFFECTED: javax.swing.JTabbedPane.
 * JDK VERSION
 * jdk-6-rc-bin-b64-linux-i586-15_dec_2005.bin
 *
 * test ran succesfully on a SUSE 7.3 Linux distribution
 *
 * Brian Harry
 * ###@###.###
 * Jan 20, 2006
 */
public class TestTab implements ChangeListener{

    JTabbedPane jtp;
    public void stateChanged(ChangeEvent ce){
	out.println(ce);
	Component c = jtp.getSelectedComponent();
	int i = jtp.getSelectedIndex();
        if(c != null){
	    out.println(c.hashCode());
	    out.println("Selected Name should be: " + jtp.getTitleAt(i));
	}
    }

    public void testTabbedPane(){

	JFrame jf = new JFrame();
	final JTabbedPane jtp = this.jtp = new JTabbedPane();
	jf.add(jtp);
	for(int i = 0; i < 10; i++)
	    jtp.addTab(String.valueOf(i), new JLabel(String.valueOf(i)));
	jtp.addChangeListener(this);
	Action remove = new AbstractAction("Remove Tab"){
		public void actionPerformed(ActionEvent ae){
		    out.println("WATCH FOR A CHANGE EVENT!:");
		    //Component c = jtp.getSelectedComponent();
		    //jtp.remove(c);
		    int index = jtp.getSelectedIndex();
		    if(index >= 0)
		         jtp.remove(index);
		    //jtp.removeAll();
		    out.println("WAS THERE A CHANGE EVENT?");
		}

	    };

	JButton jb = new JButton(remove);
	jf.add(jb, BorderLayout.SOUTH);
	jf.pack();
	jf.setVisible(true);


    }

    public static void main(String ... args){

	Runnable run = new Runnable(){
		public void run(){
		    new TestTab().testTabbedPane();
		}

	    };
	SwingUtilities.invokeLater(run);

    }


}


FIX FOR BUG NUMBER:
6368047

                                    

Comments
EVALUATION

Contribution-forum:https://jdk-collaboration.dev.java.net/servlets/ProjectForumMessageView?forumID=1463&messageID=10980
                                     
2006-01-23
EVALUATION

So I've had the opportunity to read through the peabody submission
multiple times, consider the discussion and suggested 
fixes, consider the existing buggy behavior, and then make 
what I think is the best fix possible. Here's what I've 
come up with:

A stateChanged event will be fired any time the selected 
index changes OR the actual tab changes. For example:

- selected index changes from x to y causes stateChanged
- if selected index is x and a tab before it is deleted, 
selected index changes to x - 1 and causes a stateChanged
- if selected index is x and a tab before it is inserted, 
selected index changes to x + 1 and causes a stateChanged
- if selected index is x and is deleted, selected index 
remains x, but since a new "tab" is selected, a 
stateChanged is fired

The reason that I've decided to fire stateChanged in all 
cases is that we don't know what might be important to 
developers. Some may want to know about the index, and some 
may want to know about the visible component. As such, it 
is safest to fire events in both cases.

Also, as part of the fix for 5089436, we *need* to call 
fireStateChanged() for all cases, since that's where the 
new code is to change visibility/focus.

Thanks to leouser for the help, and in particular one very 
useful line of code:

+  if(index == selected && selected == getSelectedIndex()){
+      fireStateChanged();
+  }

It took me a couple moments to grok it, but once I did it 
became *exactly* what I needed!
                                     
2006-04-11



Hardware and Software, Engineered to Work Together