Jadice Viewer - Fokus Reihenfolge der Bedienelemente

Inhaltsverzeichnis

Einleitung & Motivation

Dieser Artikel zum jadice viewer® gibt ein Überblick, wie die Fokus Reihenfolge der Viewer Bedienelemente bestimmt werden kann.
Der Artikel richtet sich an Integratoren, die aufgefordert sind, bestehende wie auch neue Anwendungen zugänglich zu gestalten.

Das hier beschriebene Konzept ermöglicht die Umsetzung von tastaturbedienbaren jadice viewer Integrationen. Sowohl die reine Tastaturbedienung als auch, die Verwendung von assistiven Technologien, wie Screenreader können dadurch ermöglicht werden. Nach unserer Erfahrung ist es wichtig und nachhaltig keine Unterscheidung der Anwendergruppen in der Umsetzung zu realisieren. Wir empfehlen, eine einheitliche Lösung für alle Anwendergruppen anzustreben. So profitieren sowohl Personen mit motorischer Einschränkung, teilweiser oder vollständiger Sehbeeinträchtigung, als auch Power-User von den hieraus abgeleiteten Lösungen.

Nachfolgend werden dafür erforderliche Konzepte und Code Beispiele erklärt.

Focus Traversal Policy

In Swing gibt es die Möglichkeit, mittels der Tastatur mit einzelnen Bedienelementen zu interagieren. Voraussetzung für die Interaktion mit einem gewünschten Bedienelement ist, dass dieses Element den Fokus hat. Dabei kann immer nur ein Bedienelement der GUI den Fokus erhalten. 
Eine Focus Traversal Policy bestimmt die Reihenfolge, in welcher die Bedienelemente den Fokus erhalten. Jede Swing-Komponente besitzt eine Default Focus Traversal Policy. Je nach Lokation der Swing Komponente in der Swing-Komponenten-Hierarchie ergibt sich daraus die Fokus Reihenfolge der Anwendung. Dieses Standardverhalten kann durch das Implementieren einer eigenen FocusTraversalPolicy verändert werden.

Eine FocusTraversalPolicy berücksichtigt nur Bedienelemente, die einen Fokus erhalten können. Das heißt, zunächst muss sichergestellt werden, dass die gewünschten Bedienelemente fokussierbar sind.
Swing sieht für Komponenten die .setFocusable(true) Methode vor.
jadice® bietet mittels des Action & Command Framework die Möglichkeit, alle Menubar-Komponenten fokussierbar zu machen. Dazu muss in der verwendeten menucomponents.properties, bzw. dessen internationalisierte Variante, folgende boolsche Einstellung gesetzt werden:

enable.focussability.for.nonfocussable.components=true

Nachfolgend finden Sie eine kleine Beispielanwendung für eine eigene FocusTraversalPolicy (siehe Abschnitt „Beispielimplementierung für eine FocusTraversalPolicy“).
Weitere Informationen zum Thema FocusTraversalPolicy finden Sie hier: https://docs.oracle.com/javase/tutorial/uiswing/misc/focus.html#customFocusTraversal

Focus Traversal Policy in jadice®

Es gibt grundsätzlich zwei Vorgehensweisen, über die der Zugriff auf die Swing-Komponenten erfolgen kann. 

  • Zum einen kann die Referenz für die Komponenten vorgehalten werden, wie es auch in der „Beispielimplementierung für eine FocusTraversalPolicy“ der Fall ist. Hierbei müssen die einzelnen Komponenten für externe Zugriffe via API aufrufbar sein.
  • Zum anderen gibt es die Möglichkeit über das Traversieren der Swing Komponenten Hierarchie mittels Identifier auf die Komponenten zuzugreifen.

In den nachfolgenden Abschnitten wird die zweite Vorgehensweise näher erläutert.

Identifizierung der Swing-Komponenten

Ist der externe Zugriff auf Swing-Komponenten aufgrund ihrer Sichtbarkeit via API nicht möglich, machen wir uns eine Eigenschaft der jadice® Implementation zunutze. Wir verwenden Informationen, die uns über den AccessibleContext der jeweiligen Komponente bereitgestellt werden. Im Besonderen nutzen wir die über die jadice Implementation bereitgestellte Eindeutigkeit des AccessibleName (In Sonderfällen kann der Accessible Context auch integrationsseitig überschrieben werden, die Eindeutigkeit des gewählten Namens obliegt in diesem Falle dem Integrator). Zu beachten ist dabei, dass der AccessibleName abhängig vom verwendeten Locale ist.

AccessibleName aus den Action & Command Framework Dateien

Für die nachfolgend beschriebene Identifizierung der Swing-Komponenten, wird der verwendete AccessibleName aus den Property Dateien des Action & Command Framework genutzt. In der actions.properties Datei wird der Name einer Action spezifiziert. Der dort verwendete Name der Action wird über die jadice implementation zu einem AccessibleName überführt. Anhand des Action Name bzw. des AccessibleName kann bei der Traversierung der Swing-Komponenten-Hierarchie die gesuchte Komponente identifiziert werden. (Siehe Abschnitt "Erstellung der Policy anhand der Action Names"
Nachfolgend ein Auschnitt einer actions.properties Datei, bei der der Action Name "Document drucken"(Zeile: 2) ausgelesen werden kann. 

actions.properties Datei
Print.commands = Print
Print.Name = Dokument drucken
Print.ShortDescription = Druckt das aktuelle Dokument
Print.SmallIcon = defaulticons.TB_PRINT

Der Action Name, bzw. Accessible Name kann wiederum im nachfolgend dargestellten "Erstellung der Policy anhand der Action Names" verwendet werden.

Erstellung der Policy anhand der Action Names
    // Store a list of references to the swing components
	List<AtomicReference<Component>> result = new ArrayList<>();
    // Accessible names we want to look for in the hierarchy
	String[] strings = {
        "Dokument drucken", "Dokumentinfo", // derived from action.properties
		"Linie" // derived from default-annotation-profile.xml
    };
	// Get the references from the traversal
    for (String s : strings) {
      result.add(walkComponents((Container) root, s));
    }
	// Pass the references as input for the policy
    FocusTraversalPolicy policy = new HierarchyFocusTraversalPolicy(result);

Im Beispiel "Erstellung der Policy anhand der Action Names" wird gezeigt, wie durch das Hinzufügen der Accessible Names in das String Array die Fokusreihenfolge definiert werden kann. Die dort definierte Reihenfolge entspricht der Reihenfolge, mit der die Swing-Komponenten den Fokus erhalten. 

AccessibleName aus dem Annotation Profil

Im Falle der AnnotationsToolbar, die dynamisch anhand des verwendeten Annotationsprofil erstellt wird, kann der AccessibleName aus dem Annotationsprofil ausgelesen werden (siehe Codebock "Auszug aus dem default-annotation-profile.xml"). Hierfür muss der Wert des attributes name (@name="Line") als AccessibleName verwendet werden (siehe Zeile 1).  

Auszug aus dem default-annotation-profile.xml
<annotation-type name="Line" archetype="Line"
                   class="com.levigo.jadice.annotation.LineAnnotation">
    <renderer toolkit="swing"
              class="com.levigo.jadice.annotation.internal.renderer.LineAnnotationRenderer"/>
    <wrangler toolkit="swing"
              class="com.levigo.jadice.swing.internal.annotation.wranglers.LineAnnotationWrangler"/>
	...
    <labels>
      <label locale="en">Line</label>
      <label locale="de">Linie</label>
      <label locale="fr">Ligne</label>
      <label locale="it">Linea</label>
    </labels>
  	...
</annotation-type>

Swing Komponenten Hierarchie Traversal

In Swing sind die einzelnen Komponenten in einer Hierarchie, die einer Baumstruktur entspricht, enthalten. Wenn man beispielsweise angefangen beim Wurzelelement der Swing Hierarchie beginnt, durch die Elemente zu traversieren, kann man mittels des zuvor erwähnten AccessibleNames alle Elemente in dieser Baumstruktur wiederfinden. Ein Beispiel, wie so etwas angegangen werden kann, wird im Abschnitt "Beispiele" gezeigt. Wir verwenden im Beispiel dabei AtomicReferences (siehe https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/AtomicReference.html), um später Zugriff auf das Originalobjekt zu erhalten.

Integrationsmöglichkeit der eigenen Focus Traversal Policy

Nachfolgend wird beschrieben wie die erstellte Focus Traversal Policy Integriert werden kann. Es wird auch auf das Verändern des Standard FocusTraversalKey eingegangen.

Setzen einer Focus Traversal Policy

Um eine FocusTraversalPolicy zu setzen, muss man nur die Policy auf der gewünschten Komponente setzen. Werden in der selben Hierarchie mehrere Komponenten mit einer Policy besetzt, so werden bei einem FocusTraversal diese Policies zunächst ignoriert. Wenn man die Beachtung dieser Policies wünscht, so kann man wie in dem gezeigten Beispiel es als PolicyProvider markieren. Dann wird bei einer Suche dessen Policy ebenfalls zur Anwendung kommen, falls diese Komponente zurückgegeben wird bei einer Suche (Next/First/Last Component).

Anwendungsbeispiel von Policies
    // create a sample hierarchy with a frame that has a panel which has 2 buttons 
	JFrame frame = new JFrame();
	JPanel panel = new JPanel();
	JButton comp1 = new JButton();
	JButton comp2 = new JButton();
	frame.add(panel);
	panel.add(comp1);
	panel.add(comp2);
	
	// set a FocusTraversalPolicy on the root of the hiearchy
	frame.setFocusTraversalPolicy(new ExampleTraversalPolicy());
	// set a different Policy on the panel. By default it will not be used when the frame has the focus and it receives a focus change
	panel.setFocusTraversalPolicy(new ExamplePanelTraversalPolicy());
	// by setting it as FocusTraversalPolicyProvider its policy will be taken into account when the panel is the component that should receive the focus next
	panel.setFocusTraversalPolicyProvider(true);

Eigene FocusTraversalKeys verwenden

Standardmäßig wird TAB verwendet, um bei der Komponente einen Fokuswechsel zur nächsten zu bewirken. Dies kann man durch das Setzen eigener Keys überschreiben. Über ein Boolean kann dabei gesteuert werden, ob die Komponente die Events des gesetzten Keys normal verarbeiten soll oder nur einen Fokuswechsel bewirken soll.

Setzen eigener FokusTraversalKeys
    // Request the preset focus traversal keys of the component
	Set<AWTKeyStroke> focusTraversalKeys = component.getFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS);
	// Create a set with CTRL+N as FocusTraversalKey and the preexisting keys of the component
    AWTKeyStroke ks = AWTKeyStroke.getAWTKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK);
    Set<AWTKeyStroke> set = new HashSet<>(focusTraversalKeys);
    set.add(ks);
	// set them as new FocusTraversalKeys
    frame.setFocusTraversalKeys(
        KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, set);
	// decides whether the key event should be propagated after the focus change action. We only want to trigger focus changes for the component in this example, so we set it to true
    frame.setFocusTraversalKeysEnabled(true);


Möglichkeiten und Grenzen

Zuvor wurde die Vorgehensweise beschrieben, bei der Anhand des AccessibleNames Komponenten während der Traversierung der Swing-Komponenten-Hierarchie aufgefunden und für eine FocusTraversalPolicy verwendet werden können. Diese Vorgehensweise ist nicht in allen Fällen anwendbar, weshalb nachfolgen Besonderheiten und Grenzen aufgezeigt werden.

Dynamische Komponenten

Bei der Konstruktion einer Focus Traversal Policy muss man bestimmten dynamischen Komponenten besondere Beachtung schenken. Mit dynamischen Komponenten sind hier Komponenten gemeint, die dynamisch anhand von äußeren Faktoren wie beispielsweise einem gesetztem Annotationsprofil erzeugt werden. Ebenfalls können bestimmte Eigenschaften von Komponenten wie beispielsweise visuelle Sichtbarkeit oder ob die Komponente deaktiviert ist, eine Relevanz bei der Reihenfolge des Fokus haben. Diese müssen bei einer Policy je nach gewünschtem Verhalten gesondert beachtet werden.

Overlays

Zum Beispiel die Annotationseditoren, das GalleryNavigationTool und die RolloutSearch unterstützen die in diesem Artikel Konzepte für die Tastaturbedienbarkeit momentan nicht. Wenn Sie diese dennoch in Ihrer Integration mit dem hier beschriebenen Konzept Verwenden wollen, stehen wir Ihnen gerne beratend zur Seite.  

Fehlende AccessibleNames

Wenn eine Swing/AWT Komponente keinen AccesibleContext oder AccessibleName vergeben hat, kann dies die Identifizierung eines entsprechenden Elements in dem Ansatz mit der Traversierung, erschweren. In diesem Fall müssen wahlweise andere Eigenschaften (AccessibleContext) einer Komponente vergeben werden, die diese in dem Hierarchiebaum eindeutig identifizierbar machen oder aber auf den Ansatz mit dem Halten von Referenzen auf die Objekte ohne Traversierung ausgewichen werden.

Externe Frames

Bei einer komplexen Anwendung kommt es häufig dazu, dass ein sekundärer Dialog oder Frame zum Einsatz kommt. Da dieses Frame oder dieser Dialog nicht Teil der Hierarche des Hauptfensters ist, muss dieses ebenfalls eine eigene Policy mitbringen, die den Fokus steuert. Wichtig ist dabei auch den Übergang zwischen den Frames zu beachten. Soll beispielsweise dem neuen Fenster Fokus gegeben werden, muss man mit requestFocus() und gegebenenfalls toFront() im Voraus das Fenster in den Vordergrund rücken. Ob ein Fenster den Fokus letztendlich bekommt, kann jedoch je nach Betriebssystem anders gehandhabt werden und die Priorität des Fensters spielt ebenfalls eine Rolle.

Hierarchie Listener

Bei manchen Swing/AWT basierten Anwendungen kann es zu einer Änderung der Swing-Hierarchie kommen. Wird eine FocusTraversalPolicy verwendet, muss diese auf derartige Änderungen eventuell reagieren müssen, je nach Anwendungsfall. Diesem Effekt kann mit einem HierarchyListener (siehe https://docs.oracle.com/javase/8/docs/api/java/awt/event/HierarchyListener.html) begegnet werden.

Beispiele

Beispielimplementierung für eine FocusTraversalPolicy

Beispielimplementierung für eine FocusTraversalPolicy
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FocusTraversalPolicy;
import java.awt.GridLayout;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;

public class AccesibleTraversal {
  public static void main(String[] args) {
    SwingUtilities.invokeLater(() -> {
      JFrame frame = new JFrame();
      frame.setLayout(new GridLayout(0, 5));
		// Create 5 buttons in order 3-1-4-2-5
      List<Component> buttons = new ArrayList<>();
      JButton button3 = new JButton("Button 3");
      frame.add(button3);
      JButton button1 = new JButton("Button 1");
      frame.add(button1);
      JButton button4 = new JButton("Button 4");
      frame.add(button4);
      JButton button2 = new JButton("Button 2");
      frame.add(button2);
      JButton button5 = new JButton("Button 5");
      frame.add(button5);
		// Hold a list for the focus order of the buttons
      buttons.add(button1);
      buttons.add(button2);
      buttons.add(button3);
      buttons.add(button4);
      buttons.add(button5);
      frame.setFocusTraversalKeysEnabled(true);
		// Policy that uses the order of the list for switching focus
      frame.setFocusTraversalPolicy(new ButtonTraversalPolicy(buttons));
      frame.setSize(new Dimension(500, 500));
      frame.setVisible(true);
    });
  }

  static class ButtonTraversalPolicy extends FocusTraversalPolicy {

    List<Component> components;

    public ButtonTraversalPolicy(List<Component> components) {
      this.components = components;
    }

    @Override
    public Component getComponentAfter(Container aContainer, Component aComponent) {
      int index = components.indexOf(aComponent);
      if (index >= 4) {
        return components.get(0);
      } else {
        return components.get(index + 1);
      }
    }

    @Override
    public Component getComponentBefore(Container aContainer, Component aComponent) {
      int index = components.indexOf(aComponent);
      if (index <= 0) {
        return components.get(components.size() - 1);
      } else {
        return components.get(index - 1);
      }
    }

    @Override
    public Component getFirstComponent(Container aContainer) {
      return components.get(0);
    }

    @Override
    public Component getLastComponent(Container aContainer) {
      return components.get(components.size() - 1);
    }

    @Override
    public Component getDefaultComponent(Container aContainer) {
      return components.get(0);
    }
  }
}

Baumsuche durch Traversierung der Swing-Hierarchie

Baumsuche in der Swing-Hierarchie
public static AtomicReference<Component> walkComponents(Container container,
      String nameToLookFor) {
    Component[] components = container.getComponents();
		// iterate through child components of the current swing component
    for (int i = 0; i < components.length; i++) {
      AccessibleContext accessibleContext = components[i].getAccessibleContext();
      if (accessibleContext != null) {
        String accessibleName = accessibleContext.getAccessibleName();
        if (nameToLookFor.equals(accessibleName)) {
		  // when a component is found we can set it to be focusable, so it can be used in the policy later on
          components[i].setFocusable(true);
          return new AtomicReference<>(components[i]);
        }
      }
	  // search in the sub tree
      AtomicReference<Component> comp = walkComponents((Container) components[i], nameToLookFor);
      if (comp != null) {
		// when a component is found we can set it to be focusable, so it can be used in the policy later on
        comp.get().setFocusable(true);
        return comp;
      }
    }
    return null;
  }

Beispielimplementierung für eine FocusTraversalPolicy mittels Traversierung der Swing-Hierarchie

FocusTraversalPolicy mittels Traversierung der Hierarchie
public class HierarchyFocusTraversalPolicy extends FocusTraversalPolicy {
    List<AtomicReference<Component>> order;

    public HierarchyFocusTraversalPolicy(List<AtomicReference<Component>> order) {
      this.order = order;
    }

    @Override
    public Component getComponentAfter(Container aContainer, Component aComponent) {
      if (!order.isEmpty()) {
        int index = getIndex(aComponent);
        if (index == -1) {
          return getDefaultComponent(aContainer);
        }
        if (index + 1 < order.size()) {
          return order.get(index + 1).get();
        } else {
          return getFirstComponent(aContainer);
        }
      }
      return null;
    }

    @Override
    public Component getComponentBefore(Container aContainer, Component aComponent) {
      if (!order.isEmpty()) {
        int index = getIndex(aComponent);
        if (index == -1) {
          return getDefaultComponent(aContainer);
        }
        if (index - 1 >= 0) {
          return order.get(index - 1).get();
        } else {
          return getLastComponent(aContainer);
        }
      }
      return null;
    }

    @Override
    public Component getFirstComponent(Container aContainer) {
      if (!order.isEmpty()) {
        return order.get(0).get();
      }
      return null;
    }

    @Override
    public Component getLastComponent(Container aContainer) {
      if (!order.isEmpty()) {
        return order.get(order.size() - 1).get();
      }
      return null;
    }

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

    public int getIndex(Component component) {
      for (int index = 0; index < order.size(); index++) {
        if (component.equals(order.get(index).get())) {
          return index;
        }
      }
      return -1;
    }
  }