# Verschachtelte JPanels FocusTraversalPolicy



## Remo84 (22. Nov 2009)

Hallo zusammen

Ich habe ein JPanel, das ein linkes und ein rechtes JPanel enthält.
Diese enthalten wiederum jeweils ein Textfeld und eine JTable.

Jedes JPanel ist innerhalb einer eigenen Klasse und die Datenfelder sind private.

Nun möchte ich mit Tab den Fokus so vergeben:
1) Textfeld des linken JPanels
2) JTable des linken JPanels
3) Textfeld des rechten JPanels
4) JTable des rechten JPanels

Nun habe ich eine eigene FocusTraversalPolicy geschrieben, die innerhalb eines JPanels auch funktioniert.

Wie kann ich nun den Fokus Panelübergreifend vergeben?

Vielen Dank
Gruss Remo


----------



## Ebenius (23. Nov 2009)

Eigentlich funktioniert das von Haus aus so wie Du willst, da brauchst Du keine eigene FocusTraversalPolicy.

```
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;

import javax.swing.*;

public class FocusFun {

  public static void main(String[] args) {
    final JPanel leftPanel = new JPanel(new BorderLayout(6, 6));
    leftPanel.add(new JTextField(10), BorderLayout.NORTH);
    leftPanel.add(new JScrollPane(createTable()), BorderLayout.CENTER);

    final JPanel rightPanel = new JPanel(new BorderLayout(6, 6));
    rightPanel.add(new JTextField(10), BorderLayout.NORTH);
    rightPanel.add(new JScrollPane(createTable()), BorderLayout.CENTER);

    final JPanel centerPanel = new JPanel(new GridLayout(1, 2, 6, 6));
    centerPanel.add(leftPanel);
    centerPanel.add(rightPanel);

    final JPanel buttonPanel =
          new JPanel(new FlowLayout(FlowLayout.TRAILING));
    buttonPanel.add(new JButton("OK"));
    buttonPanel.add(new JButton("Cancel"));

    final JPanel contentPane = new JPanel(new BorderLayout(6, 6));
    contentPane.add(centerPanel, BorderLayout.CENTER);
    contentPane.add(buttonPanel, BorderLayout.SOUTH);

    final JFrame f = new JFrame("Test Frame: FocusFun"); //$NON-NLS-1$
    f.setContentPane(contentPane);
    f.pack();
    f.setLocationRelativeTo(null);
    f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    f.setVisible(true);
  }

  private static JTable createTable() {
    final JTable table = new JTable(2, 2);
    return table;
  }
}
```
Wahrscheinlich wunderst Du Dich, dass Du aus den Tabellen mit TAB nicht mehr rauskommst. CTRL+TAB / CTRL+SHIFT+TAB wechseln den Fokus aus einer Tabelle zur nächsten / vorhergehenden Komponente. Wenn Du mit TAB / SHIFT+TAB aus egal welcher Zelle der Tabelle den Fokus wechseln möchtest -- was ich als Benutzer normaler Weise nicht mögen würde --, dann setzt Du das Key-Binding der Tabelle um:
[java=41]    final ActionMap actionMap = table.getActionMap();
    actionMap.put("focusNextComponent", new AbstractAction() {

      private static final long serialVersionUID = 1L;

      public void actionPerformed(ActionEvent e) {
        ((Component) e.getSource()).transferFocus();
      }
    });
    actionMap.put("focusPreviousComponent", new AbstractAction() {

      private static final long serialVersionUID = 1L;

      public void actionPerformed(ActionEvent e) {
        ((Component) e.getSource()).transferFocusBackward();
      }
    });

    final InputMap inputMap =
          table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
    inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0),
          "focusNextComponent");
    inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB,
          KeyEvent.SHIFT_DOWN_MASK), "focusPreviousComponent");[/code]
Vielleicht willst Du bei TAB dann der nächste Komponente den Fokus geben, wenn die letzte Zelle der Tabelle den Fokus hat und mit SHIFT+TAB dann der vorhergehende Komponente den Fokus geben, wenn die erste Zelle der Tabelle den Fokus hat. Das ginge dann bspw. so:
[java=41]    final ActionMap actionMap = table.getActionMap();
    actionMap.put("selectNextCellOrFocusNextComponent", new AbstractAction() {

      private static final long serialVersionUID = 1L;

      public void actionPerformed(ActionEvent e) {
        final JTable table = (JTable) e.getSource();
        if (table.getRowCount() - 1 == table.getSelectedRow()
              && table.getColumnCount() - 1 == table.getSelectedColumn()) {
          table.transferFocus();
        } else {
          final Action delegate =
                table.getActionMap().get("selectNextColumnCell");
          if (delegate != null) {
            delegate.actionPerformed(e);
          }
        }
      }
    });
    actionMap.put("selectPreviousCellOrFocusPreviousComponent",
          new AbstractAction() {

            private static final long serialVersionUID = 1L;

            public void actionPerformed(ActionEvent e) {
              final JTable table = (JTable) e.getSource();
              if (table.getSelectedRow() == 0
                    && table.getSelectedColumn() == 0) {
                table.transferFocusBackward();
              } else {
                final Action delegate =
                      table.getActionMap().get("selectPreviousColumnCell");
                if (delegate != null) {
                  delegate.actionPerformed(e);
                }
              }
            }
          });

    final InputMap inputMap =
          table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
    inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0),
          "selectNextCellOrFocusNextComponent");
    inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB,
          KeyEvent.SHIFT_DOWN_MASK),
          "selectPreviousCellOrFocusPreviousComponent");
[/code]
Ich hoffe, das hilft. Falls ich Dich falsch verstanden habe, frag einfach nochmal.

Ebenius


----------



## Remo84 (23. Nov 2009)

Hallo Ebenius

Vielen Dank für deine super Beispiele.

Du hast recht das funktioniert bei deinem Beispiel standardmässig schon so. 

Bei mir werden die Komponenten nicht in der "richtigen" Reihenfolge angesprungen deshalb bin ich auf die Idee mit der FocusTraversalPolicy gekommen.
Vorgestellt habe ich mir das Ganze wie eine Postorder-Traversierung. Zuerst wird die "unterste" Policy verwendet, die für das linke Panel zuständig ist. Wenn die letzte Komponente den Fokus hat, springt man eine "Ebene" nach oben. Von dort springt man wieder nach unten in die Policy des rechten Panels.

Ich hoffe das ist verständlich 
Aus reiner Neugier. Funktioniert das wie ich es mir vorgestellt habe?
Ich denke so könne ich den Fokus sehr flexibel vergeben.


Das Problem, dass ich mit Tab nicht aus der Tabelle rauskomme habe ich mit

```
table.setFocusTraversalKeys(FocusManager.FORWARD_TRAVERSAL_KEYS, null);
```
gelöst.

Gruss Remo


----------



## Ebenius (24. Nov 2009)

Remo84 hat gesagt.:


> Das Problem, dass ich mit Tab nicht aus der Tabelle rauskomme habe ich mit
> 
> ```
> table.setFocusTraversalKeys(FocusManager.FORWARD_TRAVERSAL_KEYS, null);
> ...


Stimmt, ich vergesse immer wieder, dass das auch geht. Trotzdem mein zweiter Änderungsvorschlag gefällt mir besser, da ich als Benutzer normaler Weise erwarte, innerhalb einer Tabelle mit TAB und SHIFT+TAB die Zellen wechseln zu können. Kommt aber immer drauf an, ich weiß.

Nun also zum (hoffentlich jetzt richtig verstandenen Problem): Weil Du kein Beispiel machen willst, nehme ich einfach mal mein Beispiel von oben und lege fest, dass erst die Tabelle und dann das Textfeld angesprungen werden soll. :-D Ein einfacher Vorschlag sieht so aus. Hilft Dir das?


```
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;

import javax.swing.*;
import javax.swing.FocusManager;

public class FocusFun {

  public static void main(String[] args) {
    final JTextField leftTextField = new JTextField(10);
    final JTextField rightTextField = new JTextField(10);
    final JTable leftTable = createTable();
    final JTable rightTable = createTable();

    final JPanel leftPanel = new JPanel(new BorderLayout(6, 6));
    leftPanel.add(leftTextField, BorderLayout.NORTH);
    leftPanel.add(new JScrollPane(leftTable), BorderLayout.CENTER);

    final JPanel rightPanel = new JPanel(new BorderLayout(6, 6));
    rightPanel.add(rightTextField, BorderLayout.NORTH);
    rightPanel.add(new JScrollPane(rightTable), BorderLayout.CENTER);

    final JPanel centerPanel = new JPanel(new GridLayout(1, 2, 6, 6));
    centerPanel.add(leftPanel);
    centerPanel.add(rightPanel);
    installMyFocusTraversalPolicy(centerPanel, new Component[]
          { leftTable, leftTextField, rightTable, rightTextField });

    final JPanel buttonPanel =
          new JPanel(new FlowLayout(FlowLayout.TRAILING));
    buttonPanel.add(new JButton("OK"));
    buttonPanel.add(new JButton("Cancel"));

    final JPanel contentPane = new JPanel(new BorderLayout(6, 6));
    contentPane.add(centerPanel, BorderLayout.CENTER);
    contentPane.add(buttonPanel, BorderLayout.SOUTH);

    final JFrame f = new JFrame("Test Frame: FocusFun"); //$NON-NLS-1$
    f.setContentPane(contentPane);
    f.pack();
    f.setLocationRelativeTo(null);
    f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    f.setVisible(true);
  }

  private static JTable createTable() {
    final JTable table = new JTable(2, 2);
    table.changeSelection(0, 0, false, false);
    table.setFocusTraversalKeys(FocusManager.FORWARD_TRAVERSAL_KEYS, null);
    table.setFocusTraversalKeys(FocusManager.BACKWARD_TRAVERSAL_KEYS, null);
    table.setPreferredScrollableViewportSize(new Dimension(100, 40));
    return table;
  }

  private static void installMyFocusTraversalPolicy(
        JPanel panel,
        final Component[] focusChain) {
    panel.setFocusTraversalPolicyProvider(true);
    panel.setFocusTraversalPolicy(new FocusTraversalPolicy() {
 
      @Override
      public Component getFirstComponent(Container aContainer) {
        return focusChain[0];
      }
 
      @Override
      public Component getLastComponent(Container aContainer) {
        return focusChain[focusChain.length - 1];
      }
 
      @Override
      public Component getDefaultComponent(Container aContainer) {
        return getFirstComponent(aContainer);
      }
 
      @Override
      public Component getComponentBefore(
            Container aContainer,
            Component aComponent) {
        final int len = focusChain.length;
        for (int i = 0; i < len; i++) {
          final Component test = focusChain[i];
          if (test == aComponent) {
            return focusChain[(i - 1 + len) % len];
          }
        }
        return getLastComponent(aContainer);
      }
 
      @Override
      public Component getComponentAfter(
            Container aContainer,
            Component aComponent) {
        final int len = focusChain.length;
        for (int i = 0; i < len; i++) {
          final Component test = focusChain[i];
          if (test == aComponent) {
            return focusChain[(i + 1) % len];
          }
        }
        return getFirstComponent(aContainer);
      }
    });
  }
}
```
Beachte, dass das centerPanel nur ein FocusTraversalPolicyProvider ist, aber kein CycleRoot. Happy Hacking!

Ebenius


----------



## Ebenius (24. Nov 2009)

Näher an Deinen Anforderungen (und auch sinnvoller) sind in diesem Beispiel die beiden einzelnen Panels FocusTraversalPolicyProvider:


```
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;

import javax.swing.*;
import javax.swing.FocusManager;

public class FocusFun {

  public static void main(String[] args) {
    final JTextField leftTextField = new JTextField(10);
    final JTextField rightTextField = new JTextField(10);
    final JTable leftTable = createTable();
    final JTable rightTable = createTable();

    final JPanel leftPanel = new JPanel(new BorderLayout(6, 6));
    leftPanel.add(leftTextField, BorderLayout.NORTH);
    leftPanel.add(new JScrollPane(leftTable), BorderLayout.CENTER);
    installMyFocusTraversalPolicy(leftPanel, new Component[] { leftTable,
      leftTextField });

    final JPanel rightPanel = new JPanel(new BorderLayout(6, 6));
    rightPanel.add(rightTextField, BorderLayout.NORTH);
    rightPanel.add(new JScrollPane(rightTable), BorderLayout.CENTER);
    installMyFocusTraversalPolicy(rightPanel, new Component[] { rightTable,
      rightTextField });

    final JPanel centerPanel = new JPanel(new GridLayout(1, 2, 6, 6));
    centerPanel.add(leftPanel);
    centerPanel.add(rightPanel);

    final JPanel buttonPanel =
          new JPanel(new FlowLayout(FlowLayout.TRAILING));
    buttonPanel.add(new JButton("OK"));
    buttonPanel.add(new JButton("Cancel"));

    final JPanel contentPane = new JPanel(new BorderLayout(6, 6));
    contentPane.add(centerPanel, BorderLayout.CENTER);
    contentPane.add(buttonPanel, BorderLayout.SOUTH);

    final JFrame f = new JFrame("Test Frame: FocusFun"); //$NON-NLS-1$
    f.setContentPane(contentPane);
    f.pack();
    f.setLocationRelativeTo(null);
    f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    f.setVisible(true);
  }

  private static void installMyFocusTraversalPolicy(
        JPanel panel,
        final Component[] focusChain) {
    panel.setFocusTraversalPolicyProvider(true);
    panel.setFocusTraversalPolicy(new FocusTraversalPolicy() {

      @Override
      public Component getFirstComponent(Container aContainer) {
        return focusChain[0];
      }

      @Override
      public Component getLastComponent(Container aContainer) {
        return focusChain[focusChain.length - 1];
      }

      @Override
      public Component getDefaultComponent(Container aContainer) {
        return getFirstComponent(aContainer);
      }

      @Override
      public Component getComponentBefore(
            Container aContainer,
            Component aComponent) {
        final int len = focusChain.length;
        for (int i = 0; i < len; i++) {
          final Component test = focusChain[i];
          if (test == aComponent) {
            return focusChain[(i - 1 + len) % len];
          }
        }
        return getLastComponent(aContainer);
      }

      @Override
      public Component getComponentAfter(
            Container aContainer,
            Component aComponent) {
        final int len = focusChain.length;
        for (int i = 0; i < len; i++) {
          final Component test = focusChain[i];
          if (test == aComponent) {
            return focusChain[(i + 1) % len];
          }
        }
        return getFirstComponent(aContainer);
      }
    });
  }

  private static JTable createTable() {
    final JTable table = new JTable(2, 2);
    table.changeSelection(0, 0, false, false);
    table.setFocusTraversalKeys(FocusManager.FORWARD_TRAVERSAL_KEYS, null);
    table.setFocusTraversalKeys(FocusManager.BACKWARD_TRAVERSAL_KEYS, null);
    table.setPreferredScrollableViewportSize(new Dimension(100, 40));
    return table;
  }
}
```
Natürlich ist die FocusTraversalPolicy ziemlich einfach, prüft nicht, ob die Komponente _focusable_ ist, oder _enabled_, ... Das überlasse ich dann Dir.  

[Edit] Natürlich sollten die FocusTraversalPolicies (sofern nicht stateless und mit Default Constructor erzeugbar) nicht vergessen, java.io.Serializable zu implementieren... Das hab ich mir ebenfalls geklemmt.

Ebenius


----------



## Remo84 (24. Nov 2009)

Hallo Ebenius

Sorry dass ich nicht glich ein Beispiel angehängt habe.

Dies hole ich nun nach.


```
//Left.java
import java.awt.BorderLayout;
import java.awt.Component;

import javax.swing.FocusManager;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;

public class Left extends JPanel {
	private JTextField textField;
	private JButton button;
	private JTable table;
	private JScrollPane scrollPane;
	private MyFocusTraversalPolicy focusPolicy;

	public Left() {
		System.out.println("Left erstellt");

		Component[] focusOrder = { textField, table };
		focusPolicy = new MyFocusTraversalPolicy(focusOrder);

		setFocusCycleRoot(true);
		setFocusTraversalPolicy(focusPolicy);

		setLayout(new BorderLayout(2, 2));

		button = new JButton("OK");
		textField = new JTextField("Text");
		table = new JTable(2, 2);

		table.setFocusTraversalKeys(FocusManager.FORWARD_TRAVERSAL_KEYS, null);
		scrollPane = new JScrollPane(table);

		add(textField, BorderLayout.NORTH);
		add(button, BorderLayout.CENTER);
		add(scrollPane, BorderLayout.SOUTH);
	}
}
```


```
//Right.java
import java.awt.BorderLayout;
import java.awt.Component;

import javax.swing.FocusManager;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;

public class Right extends JPanel {
	private JTextField textField;
	private JTable table;
	private JScrollPane scrollPane;
	private MyFocusTraversalPolicy focusPolicy;

	public Right() {
		System.out.println("Right erstellt");

		Component[] focusOrder = { textField, table };
		focusPolicy = new MyFocusTraversalPolicy(focusOrder);

		setFocusCycleRoot(true);
		setFocusTraversalPolicy(focusPolicy);

		setLayout(new BorderLayout(2, 1));

		textField = new JTextField("Text");
		table = new JTable(2, 2);
		table.setFocusTraversalKeys(FocusManager.FORWARD_TRAVERSAL_KEYS, null);
		scrollPane = new JScrollPane(table);

		add(textField, BorderLayout.NORTH);
		add(scrollPane, BorderLayout.SOUTH);
	}
}
```



```
//Master.java
import java.awt.BorderLayout;
import java.awt.Component;

import javax.swing.JPanel;

public class Master extends JPanel {
	private Left leftPanel;
	private Right rightPanel;
	private MyFocusTraversalPolicy focusPolicy;

	public Master() {
		Component[] focusOrder = { leftPanel, rightPanel };
		focusPolicy = new MyFocusTraversalPolicy(focusOrder);

		setFocusCycleRoot(true);
		setFocusTraversalPolicy(focusPolicy);

		setLayout(new BorderLayout(6, 6));

		leftPanel = new Left();
		rightPanel = new Right();

		add(leftPanel, BorderLayout.EAST);
		add(rightPanel, BorderLayout.WEST);
	}
}
```



```
//Main.java
import javax.swing.JFrame;

public class Main extends JFrame {
	private Master masterPanel;

	public Main() {
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setSize(300, 300);
		setVisible(true);

		masterPanel = new Master();

		add(masterPanel);

		pack();
	}

	public static void main(String[] args) {
		Main main = new Main();
	}
}
```


```
//MyFocusTraversalPolicy.java
import java.awt.Component;
import java.awt.Container;
import java.awt.FocusTraversalPolicy;

public class MyFocusTraversalPolicy extends FocusTraversalPolicy {
	final private Component[] focusChain;

	public MyFocusTraversalPolicy(Component[] focusOrder) {
		focusChain = focusOrder;
	}

	@Override
	public Component getFirstComponent(Container aContainer) {
		return focusChain[0];
	}

	@Override
	public Component getLastComponent(Container aContainer) {
		return focusChain[focusChain.length - 1];
	}

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

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

		final int len = focusChain.length;
		for (int i = 0; i < len; i++) {
			final Component test = focusChain[i];
			if (test == aComponent) {
				return focusChain[(i - 1 + len) % len];
			}
		}
		return getLastComponent(aContainer);
	}

	@Override
	public Component getComponentAfter(Container aContainer,
			Component aComponent) {
		System.out.println("getComponentAfter");
		final int len = focusChain.length;
		for (int i = 0; i < len; i++) {
			final Component test = focusChain[i];
			if (test == aComponent) {
				return focusChain[(i + 1) % len];
			}
		}
		return getFirstComponent(aContainer);
	}
}
```

Nun wollte ich, wenn ich TAB drücke, als erstes die Policy vom Master Panel holen und dort die erste Komponente rauslesen. Also das leftPanel. 
Von dort geht es weiter nach "unten". Also wird von der Policy vom leftPanel die erste Komponente geholt (das textField).
Drücke ich nun das zweite Mal die TAB-Taste wird der Fokus der table gegeben und somit die letzte Komponente im linken Panel erreicht.
Ein erneutes TAB springt wieder nach "oben" in die Policy des Master Panels. Dort wird die zweite Komponente ausgewählt (rightPanel).
Nun das gleiche Spiel wie mit dem leftPanel. Der Fokus wird dem textField gegeben.
Hat der Fokus die letzte Komponente erreicht (table) beginnt das ganze von Vorne.

Hab ich zu hohe Erwartungen an die FocusTraversalPolicy ? 

Gruss Remo


----------



## Ebenius (25. Nov 2009)

Nein, das geht zu realisieren. Allerdings funktioniert die FokusTraversalPolicy nicht ganz so wie Du denkst... Deine Policy des Master-Panels selektiert die Panels. Das darf sie nicht tun. Sie muss selbst herausfinden, dass die Panels nicht fokussierbar sind, und dann an deren (möglicher Weise über die Container-Hierarchy angebotene) FokusTraversalPolicy übergeben. Ich schrieb ja oben, die Policies sind schon komplizierter. Schau doch einfach mal in den Quelltext der [c]java.awt.ContainerOrderFocusTraversalPolicy[/c]; dort siehst Du, wie die Policies arbeiten müssen, wenn sie sich um alles generisch kümmern; _visible_, _enabled_, _focusable_, _focusCycleRoot_, _focusTraversalPolicyProvider_...

Warum Deine Panels alle _FocusCycleRoot_s sein müssen, hab ich noch nicht verstanden. Mach sie einfach zu _FocusTraversalPolicyProvider_s, das ergibt IMHO viel mehr Sinn.

Ebenius


----------

