Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

jTabbedPane transfer focus to next sheet

Tags:

java

swing

Problem Description

I am currently writing an application used to manually copy data from a sheet of paper to a database. This application has plenty of widgets where the user can enter the data. To keep the UI somewhat tidy, I decided to use a tabbed pane, separating the entry fields into logical units.

The most important feature of the application is that is should be usable by keyboard alone. So you should be able to switch tabs with keystrokes. By default this is possible using CTRL+PgUp/PgDown. But, as an additional convenience, I would like to activate the next tab as soon as the user transfers focus out of the last widgets on the current tab.

So, if the user has the focus on the last text field, and presses tab, I'd like to activate the next tab, and put the focus on the first widget therein.

To solve this, I marked the jTabbedPane as focusCycleRootProvider and added a custom FocusTraversalPolicy. My current problem is this: As soon as I programatically activate the next tab (using setSelectedIndex), which happens in the getComponentAfter method, the method getComponentAfter is executed a second time. This defeats my current logic. I cannot seem to find a way to prevent this from happening. Any ideas?

In the example below, you will see an ArrayIndexOutOfBoundsException. This happens because getComponentAfter is called twice. Once on the first tab, and once on the second tab. But twice with the same component as argument. This means, that the second time, the for-loop won't find a matching component, so the counter i will be as large as there are components in the second tab + 1. This causes the exception.

Test Executable

/*
 * TestFrame.java
 *
 * Created on Apr 18, 2011, 4:37:52 PM
 */

package testrun;

import java.awt.Component;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;

/**
 *
 * @author malbert
 */
public class TestFrame extends javax.swing.JFrame {

    /** Creates new form TestFrame */
    public TestFrame() {
        initComponents();
        jTabbedPane1.setFocusTraversalPolicyProvider(true);
        jTabbedPane1.setFocusTraversalPolicy(new EasyTabberFocusTraversalPolicy(jTabbedPane1));

        jTabbedPane1.addFocusListener(new FocusAdapter() {

            @Override
            public void focusGained(FocusEvent e) {
                super.focusGained(e);
                jTabbedPane1.setSelectedIndex(0);
                Component ca = jTabbedPane1.getFocusTraversalPolicy().getFirstComponent(jTabbedPane1);
                if (ca != null) {
                    ca.requestFocusInWindow();
                }
            }
        });
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        jTabbedPane1 = new javax.swing.JTabbedPane();
        jPanel1 = new javax.swing.JPanel();
        jTextField2 = new javax.swing.JTextField();
        jTextField3 = new javax.swing.JTextField();
        jPanel2 = new javax.swing.JPanel();
        jButton1 = new javax.swing.JButton();
        jButton2 = new javax.swing.JButton();
        jTextField4 = new javax.swing.JTextField();
        jTextField1 = new javax.swing.JTextField();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

        jTextField2.setText("jTextField2");

        jTextField3.setText("jTextField3");

        javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1);
        jPanel1.setLayout(jPanel1Layout);
        jPanel1Layout.setHorizontalGroup(
            jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel1Layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jTextField2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(jTextField3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addContainerGap(289, Short.MAX_VALUE))
        );
        jPanel1Layout.setVerticalGroup(
            jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel1Layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(jTextField2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jTextField3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addContainerGap(168, Short.MAX_VALUE))
        );

        jTabbedPane1.addTab("tab1", jPanel1);

        jButton1.setText("jButton1");

        jButton2.setText("jButton2");

        jTextField4.setText("jTextField4");

        javax.swing.GroupLayout jPanel2Layout = new javax.swing.GroupLayout(jPanel2);
        jPanel2.setLayout(jPanel2Layout);
        jPanel2Layout.setHorizontalGroup(
            jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel2Layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(jPanel2Layout.createSequentialGroup()
                        .addComponent(jButton1)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(jButton2))
                    .addComponent(jTextField4, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addContainerGap(165, Short.MAX_VALUE))
        );
        jPanel2Layout.setVerticalGroup(
            jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel2Layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(jButton1)
                    .addComponent(jButton2))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jTextField4, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addContainerGap(162, Short.MAX_VALUE))
        );

        jTabbedPane1.addTab("tab2", jPanel2);

        jTextField1.setText("jTextField1");

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jTabbedPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 376, Short.MAX_VALUE)
                    .addComponent(jTextField1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(jTextField1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jTabbedPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 251, Short.MAX_VALUE)
                .addContainerGap())
        );

        pack();
    }// </editor-fold>//GEN-END:initComponents

    /**
    * @param args the command line arguments
    */
    public static void main(String args[]) {
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                new TestFrame().setVisible(true);
            }
        });
    }

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JButton jButton1;
    private javax.swing.JButton jButton2;
    private javax.swing.JPanel jPanel1;
    private javax.swing.JPanel jPanel2;
    private javax.swing.JTabbedPane jTabbedPane1;
    private javax.swing.JTextField jTextField1;
    private javax.swing.JTextField jTextField2;
    private javax.swing.JTextField jTextField3;
    private javax.swing.JTextField jTextField4;
    // End of variables declaration//GEN-END:variables

}

Traversal Policy

package testrun;

import java.awt.Component;
import java.awt.Container;
import javax.swing.JTabbedPane;
import javax.swing.LayoutFocusTraversalPolicy;

/**
 *
 * @author malbert
 */
public class EasyTabberFocusTraversalPolicy extends LayoutFocusTraversalPolicy {

    private final JTabbedPane container;
    private int currentTab = 0;

    public EasyTabberFocusTraversalPolicy(JTabbedPane container) {
        this.container = container;
    }

    @Override
    public Component getComponentAfter(Container aContainer, Component aComponent) {
        System.out.println("after " + aComponent);
        Component comp = container.getComponentAt(currentTab);
        if (Container.class.isInstance(comp)) {
            Component[] components = ((Container) comp).getComponents();
            int i = 0;
            for (i = 0; i < components.length; i++) {
                if (!components[i].isEnabled() || !components[i].isFocusable()) {
                    continue;
                }
                if (components[i].equals(aComponent)) {
                    break;
                }
            }
            if (i == components.length - 1) {
                // we reached the end. Go to the next tab!
                currentTab = currentTab + 1;
                Component fc = firstComponentInCurrentTab();
                activateTab(currentTab);
                return fc;
            } else {
                return components[i + 1];
            }
        } else {
            return comp;
        }
    }

    @Override
    public Component getComponentBefore(Container aContainer, Component aComponent) {
        System.out.println("before");
        return super.getComponentBefore(aContainer, aComponent);
    }

    @Override
    public Component getFirstComponent(Container aContainer) {
        System.out.println("first");
        return firstComponentInCurrentTab();
    }

    @Override
    public Component getLastComponent(Container aContainer) {
        System.out.println("last");
        return lastComponentInCurrentTab();
    }

    private Component firstComponentInCurrentTab() {
        Component comp = container.getComponentAt(currentTab);
        if (comp instanceof Container) {
            Component[] components = ((Container) comp).getComponents();
            if (components.length == 0) {
                return null;
            }
            return components[0];
        } else {
            return comp;
        }
    }

    private Component lastComponentInCurrentTab() {
        Component comp = container.getComponentAt(currentTab);
        if (comp instanceof Container) {
            Component[] components = ((Container) comp).getComponents();
            if (components.length == 0) {
                return null;
            }
            return components[components.length - 1];
        } else {
            return comp;
        }
    }

    private void activateTab(int index) {
        // wrap around
        if (index < 0) {
            index = container.getTabCount() - 1;
        } else if (index > container.getTabCount() - 1) {
            index = 0;
        }
        currentTab = index;
        container.setSelectedIndex(index);
    }
}
like image 835
exhuma Avatar asked Apr 18 '11 14:04

exhuma


1 Answers

FTPs are ... a pain where it really hurts ;-)

Not entirely sure what exactly causes the NPE, just streamlined your code (if you have a method to access the last, better use it) a bit to play with:

@Override
public Component getComponentAfter(Container aContainer, Component aComponent) {
    System.out.println("after " + aComponent);
    Component last = lastComponentInCurrentTab();
    if (aComponent == last) {
      // we reached the end. Go to the next tab!
      currentTab = currentTab + 1;
      Component fc = firstComponentInCurrentTab();
      activateTab(currentTab);
      return fc;

    }
    return super.getComponentAfter(aContainer, aComponent);
}

The AIOOB is gone, the second tab is shown ... but not focused. The reason this happens is (guess only, as I have seen similar thingies in other setups, without remembering all the dirty details ;) that at the time of returning the first comp of the second tab is not yet eligable for receiving the focus, so it's further transferred to the text outside of the tab.

edit: have to revise my guess - after a bit of digging, problem seems to be the incorrect override of getFirst/LastComponent. They have to return the first/last of all tabs, that is the first always the first of the first tab and the last always the last of the last tab. Here the snippet for first:

@Override
public Component getFirstComponent(Container aContainer) {
    System.out.println("first");
    return firstComponentInTab(0) ;//firstComponentInCurrentTab();
}

private Component firstComponentInCurrentTab() {
    int tabIndex = currentTab;
    return firstComponentInTab(tabIndex);
}

private Component firstComponentInTab(int tabIndex) {
    Component comp = container.getComponentAt(tabIndex);
    LOG.info("comp: " + comp.getName());
    if (comp instanceof Container) {
        Component[] components = ((Container) comp).getComponents();
        if (components.length == 0) {
            return null;
        }
        return components[0];
    } else {
        return comp;
    }
}

then the forward tabbing looks fine. Similar needed for last, cleanup left to OP :-)

BTW: nice idea!

Edit2: beware - the cleanup will not be trivial. FTPs nust handle the complete hierarchies of its children, so checking for direct children only will break very soon (f.i. for compound components like JComboBox)

like image 78
kleopatra Avatar answered Oct 30 '22 15:10

kleopatra