Die Tool-API, zweiter Teil: Wir schnitzen uns ein Werkzeug

Zum Thema Mausgesten scheint es nur zwei Haltungen zu geben: Entweder man liebt sie oder man hasst sie. Vielleicht geht es Ihnen ja wie mir: Ich benutze mit Begeisterung das Plugin "All-in-one Gestures" für Firefox und bin immer wieder irritiert, wenn die Gesten, die im Browser ein so flüssiges und effizientes Navigieren erlauben, in meiner favorisierten IDE nichts bewirken. Aber auch bei jadice 5 sieht es in Sachen Mausgesten noch zappenduster aus. Aber diesen Misstand werden wir heute beseitigen: Mit der neuen Tool-API werden wir jadice 5 die Unterstützung von Mausgesten beibringen.

Es sei gleich vorweg geschickt: Die hier besprochene Implementierung der Mausgestenunterstützung erhebt weder Anspruch auf Vollständigkeit noch auf perfekte Funktion. Auch über die Sinnhaftigkeit kann gestritten werden: Braucht ein Dokumentenbetrachter Mausgesten? Wie dem auch sei – ein geeignetes Beispiel geben sie allemal ab.

Der Quelltext des Beispiels finden Sie beim nächsten Release von jadice 5 (beta-1) im Modul demo-client und dort im Package com.levigo.jadice.demo.gestures, sowie im Anhang zu diesem Artikel.

Was kann das MouseGestureTool?

Wie bereits gesagt, handelt es sich bei dem MouseGestureTool nur um ein Beispieltool. Nichts desto trotz unterstützt es eine ganze Reihe von Gesten. Um eine Geste auszuführen, drücken und halten Sie die rechte Maustaste, ziehen Sie die Maus grob entlang einem der folgenden Gestenpfade – wobei die einzelnen Abschnitte mindestens 50 Pixel lang sein müssen – und lassen Sie die rechte Maustaste wieder los.

Folgende Gesten werden unterstützt:

GesteFunktionVisualisierung
EZur nächsten Seite blättern
WZur vorigen Seite blättern
EWZur letzten Seite blättern
WEZur ersten Seite blättern
ESSeite nach rechts drehen
WSSeite nach links drehen
SEDokument schließen
SWDokument aus Datei öffnen
NMiniaturansicht ein/ausschalten

Die Gestenerkennung

Auf die Implementierung der Gestenerkennung selbst möchte ich in diesem Artikel nicht in allzu großer Tiefe eingehen. Sie ist allerdings in der Tat relativ einfach gehalten und umfasst lediglich etwa vier Bildschirmseiten Code. Die Gestenerkennung basiert auf der Einteilung der möglichen Bewegungsrichtung in vier Himmelsrichtungen. Gesten bestehen aus mehreren Bewegungssegmenten, die jeweils in eine der vier Himmelsrichtungen zeigen. Zwei aufeinanderfolgende Segmente haben niemals die gleiche Richtung, da diese beiden Segmente zu einem einzigen Segment zusammengefasst werden.

Die zentrale Klasse ist MouseGestureSupport. An ihr werden zunächst MouseGestureListeners registriert, die mit Informationen über im Gange befindliche oder abgeschlossene erkannte Gesten versorgt werden. Hier die Definition der Schnittstelle:

/**
 * MouseGestureListeners are notified about mouse gestures in progress and recognized gestures.
 */
public interface MouseGestureListener {
  /**
   * Receive notification about a gesture in progress on the given component. This method is called
   * many times during the recognition phase.
   *
   * @param source the component on which the gesture is executed
   * @param gesture the gesture, as recognized so far
   * @param path the path the mouse traveled, relative to the source component
   */
  void mouseGestureInProgress(Component source, String gesture, GeneralPath path);

  /**
   * Receive notification about an aborted recognition attempt. This may be due to an invalid
   * gesture or an invalid modifier state change.
   */
  void mouseGestureAborted();

  /**
   * Receive notification about a recognized gesture.
   *
   * @param source the component on which the gesture was executed
   * @param gesture the recognized gesture
   * @param path the path the mouse traveled, relative to the source component
   */
  void mouseGestureRecognized(Component source, String gesture, GeneralPath path);
}

Im nächsten Schritt muss der MouseGestureSupport mit Mausereignissen versorgt werden. Hierfür fordert man von MouseGestureSupport eine MouseEventReceiver an. Der MouseEventReceiver erweitert die Schnittstellen MouseListener und MouseMotionListener, bietet aber als alternative "Anlieferungsmethode" noch die Methode void processMouseEvent(MouseEvent e). Je nach Situation, kann der MouseEventReceiver also als Mouse(Motion)Listener an der Komponente, die Gesten unterstützen soll, registriert werden, oder es können manuell Events an processMouseEvent übergeben werden.

MouseGestureSupport kann nun noch mit einer Maske konfiguriert werden, die angibt, bei welchen Maustasten und bei welchem Zustand der Umschalttasten Gesten erkannt werden sollen. Standardmäßig werden Gesten bei gedrückter rechter Maustaste ohne Umschalttasten erkannt. Um z.B. mit der linken Maustaste in Verbindung mit gedrückter Shift-Taste Gesten zu erkennen, setzen Sie die Maske wie folgt:

mouseGestureSupport.setButtonAndModifierMask(
MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.SHIFT_DOWN_MASK
);

Ein sehr einfaches Beispiel, wie die Gestenerkennung eingesetzt werden kann, finden Sie im meiner Testklasse GestureTester.

Das MouseGestureTool

Kommen wir nun zum Wesentlichen Teil dieses Artikels – zu unserem MouseGestureTool. Das MouseGestureTool habe ich von AbstractTool abgeleitet, was den Implementierungsaufwand deutlich reduziert. Dies empfehlen wir auch grundsätzlich für andere Implementierungen, da wir es uns vorbehalten, das Interface Tool mit weiteren Methoden auszustatten. Das AbstractTool wird hierfür immer sinnvolle Standardimplementierungen bereitstellen.

Das MouseGestureTool initialisiert sich zunächst, indem es einen MouseGestureSupport erzeugt und daran einen MouseGestureListener registriert. Anschließend erzeugt es einen MouseEventReceiver, der später mit Ereignissen beschickt wird.

Schauen wir uns zunächst den MouseGestureListener des Tools an:

    mouseGestureSupport.addMouseGestureListener(new MouseGestureListener() {
      public void mouseGestureInProgress(Component source, String gesture, GeneralPath path) {
        // while a gesture is in progress no other tool should interfere with us
        if (!manager.isExclusive())
          manager.setExclusive(MouseGestureTool.class);
       
// remember path, so we can visualize it later
MouseGestureTool.this.path = path;

        if (showGesturePath)
          manager.getViewComponent().repaint(path.getBounds());
      }

      public void mouseGestureAborted() {
        manager.setNonExclusive(MouseGestureTool.class);
        path = null;
      }
      public void mouseGestureRecognized(Component source, String gesture, GeneralPath path) {
        manager.setNonExclusive(MouseGestureTool.class);
path = null;
        handleGesture(source, gesture, path);
      }
    });
  • In mouseGestureInProgress sehen wir ein Beispiel, wie der Exklusivmodus des Tools genutzt wird: Sobald erkannt wird, dass gerade eine Geste ausgeführt wird und sich der Manager nicht im Exklusivmodus befindet, wird das MouseGestureTool als das alleinig aktive Tool gesetzt. Darüber hinaus wird der bisherige Pfad der Geste gespeichert, sodass dieser später visualisiert werden kann. Sofern die Gestenvisualisierung eingeschaltet ist (showGesturePath), wird ein Neuzeichnen der ViewComponent veranlasst.
  • In MouseGestureAborted genügt es, den gespeicherten Pfad zu verwerfen und den Exklusivmodus zu verlassen. Der Repaint wird vom ToolManager aufgrund der Zustandsänderung automatisch veranlasst.
  • In MouseGestureRecognized verlassen wir ebenfalls den Exklusivmodus und delegieren ansonsten die Verarbeitung der Geste an die handleGesture-Methode des Tools.

Die Ereignisverarbeitung

Die Verarbeitung von Eingabeereignissen ist beim MouseGestureTool besonders einfach, da zum einen der MouseGestureSupport ohnehin den Großteil der Arbeit übernimmt und wir zum anderen auf die "catch-all"-Verarbeitung zurückgreifen können. Die gesamte Implementierung benötigt lediglich folgende vier Zeilen:

  public void handleEditEvent(Class<?> context, boolean isActive, EditEvent e) {
    if (e.getInputEvent() instanceof MouseEvent)
      eventReceiver.processMouseEvent((MouseEvent) e.getInputEvent());
  }

Anstatt die einzelnen mousePressed/mouseMoved/mouseDragged Template-Methoden des AbstractTool zu überschreiben, begnügen wir uns damit, direkt über die handleEditEvent-Methode alle Events abzufangen. Anschließend müssen wir nur noch prüfen, ob es sich bei dem Event um einen MouseEvent handelt, da der EventReceiver nur solche "frisst". Letzterem werfen wir den MouseEvent dann ggf. vor. Für das MouseGestureTool machen wir hier nicht den Versuch, das Ereignis zu konsumieren, da ohnehin bei Erkennung des Beginns einer Geste in den Exklusivmodus geschaltet wird. 

Die Gestenvisualisierung

Zur Visualisierung einer Geste, die gerade im Gange ist, überschreiben wir die render-Methode der Tool-Schnittstelle:

  public void render(RenderParameters parameters, boolean isActive) {
    if (null != path) {
      final Graphics2D g2 = parameters.getGraphics();

      // g2's origin is currently located at the page origin.
      // we need to translate to the view component's origin.
      final Rectangle b = parameters.getRenderedPageBounds();
      g2.translate(-b.x, -b.y);
      g2.setColor(Color.GREEN);
      g2.draw(path);
      g2.translate(b.x, b.y);
    }
  }

Sofern gerade eine Geste im Gange ist (path != null), entnehmen wir den RenderParameters einen Grafikkontext, konfigurieren ihn und zeichen den Pfad. Dies wird durch den Umstand verkompliziert, dass das Rendering der Tool-API darauf ausgelegt ist, dass in der Regel im Zusammenhang mit einer bestimmten Seite gerendert werden soll. Der Grafikkontext hat seinen Ursprung deshalb am Ursprung der Seite und nicht am Ursprung der ViewComponent. Wir verschieben ihn deshalb zunächst entsprechend und machen die Verschiebung danach wieder rückgängig.

Die Handhabung der Gesten

Die eigentliche Handhabung der erkannten Gesten ist zwar der wesentliche Teil des Tools, aber relativ simpel, da die meiste Arbeit bereits erledigt ist. Der MouseGestureSupport liefert erkannte Gesten als Zeichenketten aus Himmelsrichtungen an. Eine Geste, die zunächst nach oben zeigt, danach nach rechts und schließlich nach unten wird als Nord-Ost-Süd, also "NES" angeliefert. Die handleGesture-Methode muss diese Zeichenketten nun nur noch auswerten:

  protected void handleGesture(Component source, String gesture, GeneralPath path) {
    final ViewComponent vc = manager.getViewComponent();
    final Document doc = vc.getDocument();
    if (doc != null) {
      if (vc instanceof PageView) {
        final PageView pv = (PageView) vc;
        if (gesture.equals("E")) {
          // next page
          pv.setCurrentPageIndex(min(pv.getCurrentPageIndex() + 1, doc.getPageCount() - 1));

[...]

        } else if (gesture.equals("ES")) {
          // rotate CW
          final BaseRenderSettings brs = pv.getCurrentPageControls().getBaseRenderSettings();
          brs.setRotation(brs.getRotation() == Rotation.ROT_270
              ? Rotation.ROT_000
              : Rotation.values()[brs.getRotation().ordinal() + 1]);

[...]

        } else if (gesture.equals("N")) {
          // toggle sorter
          new ToggleSorterCommand().execute(Context.get(pv));
        }
      }
    }
  }

Die meisten Implementierungen habe ich der Übersichtlichkeit halber weggelassen. Sie finden Sie im Quelltext. Über die Instanzvariable "manager" des AbstractTools haben wir stets Zugriff auf den ToolManager und damit auf die zugehörige ViewComponent. Die Gesten sind darauf ausgelegt, mit der PageView zusammen zu arbeiten. Wir prüfen deshalb, ob das Tool eine PageView als ViewComponent hat. Andere Gesten könnten selbstverständlich auch mit der ThumbnailView zusammenarbeiten. Nun müssen wir nur noch anhand der erkannten Geste verzweigen und die entsprechende Operation durchführen.

Das MouseGestureTool registrieren

Das MouseGestureToolist kein Tool, das standardmäßig von der ViewComponent registriert wird. Wir müssen dies deshalb manuell tun. Der Aufruf dazu ist zum Glück relativ einfach. Sie finden ihn in JadicePanel.initializeGUI(String[]) in Zeile 142:

    mainViewerPanel.getPageView().getToolManager().register(MouseGestureTool.class, true);

Nun steht der Verwendung der Mausgesten nichts mehr im Wege.