Viele der augenfälligsten Neuerungen von jadice 5 betreffen die Bedienung bzw. die Benutzeronerfläche der Views, zuvorderst dabei sicher den PageView. Die Views selbst steuern dabei jedoch nur einen begrenzten Teil des Verhaltens bei. Der weitaus größere Teil wird von unabhängigen Komponenten bereitgestellt, die an die einzelnen Views angedockt werden können, um deren Funktionsumfang zu erweitern oder anzupassen. In jadice 4 existierte bereits ein API, das eine ganz ähnliche Aufgabenbenbeschreibung hatte, nämlich das Gespann aus EditEventListener und EditPane. Jadice 5 stellt hierfür ein grundlegend überarbeitetes API zur Verfügung, das nicht nur mächtiger, sondern auch einfacher zu verwenden ist – das Tool-API. In diesem Artikel werden wir zunächst die neue Tool-API vorstellen und im zweiten Teil anhand eines kleinen Fallbeispiels darstellen, wie sie zur Implementierung eigener Werkzeuge benutzt werden kann.
Das Tool
Es liegt nahe, unseren Rundgang bei der zentralen Schnittstelle für Werkzeuge, Tool, zu beginnen. Tools haben vier wesentliche Aufgabenbereiche:
- die Verarbeitung von Eingabeereignissen
- das Anreichern der Darstellung des Views mit eigenen Elementen, d.h. die Teilname am Rendering des Views
- das Beitragen von Kontextmenüelementen
- die Teilnahme an der Zustandsverwaltung im Zusammenspiel mit dem ToolManager
Bis auf den letzten Punkt sind alle Aufgaben optional, d.h. Tools müssen z.B. nicht zwingend auf Eingaben reagieren oder visuelle Beiträge zur Oberfläche liefern, allerdings müssen Tools die entsprechenden Methoden gemäß der Schnittstelle Tool implementieren. Wir empfehlen deshalb die Implementierung eigener Tools nicht auf die Schnittstelle, sondern auf die abstrakte Basisklasse AbstractTool zu stützen.
Eingabeereignisse
Tools werden von den Views mit Eingabeereignissen versorgt, wodurch sie die Möglichkeit erhalten, auf Eingaben zu reagieren. Die von AWT/Swing bekannte Klassenhierarchie für Ereignisse (InputEvent und die davon abgeleiteten Klassen) bildet die Basis für die Eingabeereignisse. Allerdings werden diese nicht direkt verwendet, da Tools für ihre Arbeit in der Regel weitere Kontextinformationen, wie die betroffene Seite (Page), die aktuellen RenderControls etc. benötigen. Aus diesem Grund stellt jadice den InputEvents eine analoge Klassenhierarchie unterhalb der abstrakten Klasse EditEvent zur Seite: KeyEditEvent, MouseEditEvent und MouseWheelEditEvent. Jeder EditEvent bietet jedoch immer auch den Zugriff auf den entsprechenden InputEvent. EditEvents beziehen sich meist auf eine Page, z.B. dann, wenn sich der Mauszeiger in der Nähe oder über der Darstellung einer Seite befindet, allerdings können diese auch ohne Seitenkontext auftreten, weshalb Tools mit diesem Umstand umgehen können müssen. In der Praxis treten bei der Ereignisverarbeitung im Zusammenhang mit in Views dargestellten Seiten immer wieder ähnliche Anforderungen zutage. Die EditEvents versuchen hierbei die häufigsten Anforderungen auf komfortable Weise abzudecken. Beispielsweise bietet der MouseEditEvent fünf verschiedene Methoden zum Zugriff auf die Koordinaten der Maus:
- getInputEvent().getPoint() entspricht den von AWT/Swing gewohnten Koordinaten in Pixeln, bezogen auf die obere linke Ecke des Views.
- getPoint() liefert die Bildschirmkoordinaten (Pixel) bezogen auf die obere linke Ecke der Seite, auf die sich der Event bezieht.
- getConstrainedPoint() liefert die gleichen Werte wie getPoint(), allerdings werden diese, wenn sich die Maus außerhalb einer Seite befindet, so begrenzt, dass der nächstgelegene Punkt innerhalb der nächstgelegenen Seite geliefert wird.
- getDocumentPoint() liefert die Dokumentkoordinaten, d.h. Koordnaten in Einheiten von Document.BASE_RESOLUTION, bezogen auf die obere linke Ecke der nächstgelegenen Seite.
- getConstrainedDocumentPoint() entspricht wieder getConstrainedPoint(), allerdings findet hier wieder das "Einfangen" der Koordinaten wie bei getConstrainedPoint() statt.
Die Schnittstelle Tool definiert für die Übergabe von EditEvents lediglich die Methode handleEditEvent, der der Event übergeben wird. Eine weitere Verteilung nach Typ des Ereignisses wie z.B. für MouseListener findet zunächst nicht statt. Allerdings nimmt die Standardimplementierung der handleEditEvent-Methode des AbstractTools eine Verteilung an Template-Methoden wie keyPressed, mouseClicked usw. vor. Tools-Implementierungen, die von AbstractTool abgeleitet sind, können deshalb sehr komfortabel selektiv nur diejenigen Ereignismethoden überschreiben, die das Tool tatsächlich benötigt.
Verteilungsreihenfolge und Konsumieren von Ereignissen
Da in einer View häufig mehr als ein Tool registriert und auch aktiv ist, ist es wichtig, die Reihenfolge, in der die verschiedenen Tools mit Ereignissen versorgt werden, beeinflussen zu können. Tools können weitgehend selbst steuern, wie früh oder spät in der Reihenfolge sie mit Ereignissen versorgt werden. Die Methode getDispatchPriority() des Tools muss hierzu einen Wert zwischen Tool.MIN_PRIORITY und Tool.MAX_PRIORITY zurückgeben. Je näher an MAX_PRIORITY der Wert ist, desto früher wird das Tool mit Ereignissen versorgt.
Hat ein Tool ein Ereignis erhalten und hat es sich dafür entschieden, darauf zu reagieren, ist es oft sinnvoll dafür zu sorgen, dass keine anderen Tools mehr auf das Ereignis reagieren. Hierzu genügt es, beim Verarbeiten des Ereignisses dessen consume()-Methode aufzurufen. Sobald ein Ereignis konsumiert wurde, wird es nicht mehr an weitere Tools zur Verarbeitung übergeben. Im Umkehrschluss bedeutet dies auch, dass Tools nicht explizit prüfen müssen, ob ein Ereignis bereits konsumiert wurde, da sie dieses sonst überhaupt nicht erhalten hätten.
Die Darstellung visueller Elemente (Rendering)
Nachdem Views die Seitendarstellungen gerendert haben, erhalten Tools die Gelegenheit, eigene visuelle Elemente beizutragen. Für jede sichtbare Seite wird zu diesem Zweck die Tool-Methode render aufgerufen, die ein Parameterobjekt (RenderParameters) sowie die Information, ob das Tool gerade aktiv ist, übergeben bekommt. Die RenderParameters enthalten alle Kontextinformationen, die zum Rendering benötigt werden, z.B. den Zielgrafikkontext (Graphics2D), die Seite, den Bereich, in den die Seite gerendert wurde usw.
Rendering-Reihenfolge
Genauso, wie es eine Dispatch-Reihenfolge für die Verteilung von Ereignissen gibt, existiert eine separate Reihenfolge für das Zeichnen der Tools. Über getRenderPriority() kann ein Tool wiederum festlegen, wie früh oder spät es beim Rendering zum Zug kommen will. In der Regel sollten die Dispatch- und Renderreihenfolge in einer inversen Beziehung zueinander stehen, da Tools konzeptionell wie als Ebenen betrachtet werden können: je weiter "oben" desto früher werden Ereignisse verteilt und desto später wird gerendert. Das AbstractTool unterstützt dieses Verhalten indem die Standardimplementierung von getDispatchPriority() den Wert MAX_PRIORITY-getRenderPriority() zurückgibt. Implementierungen müssen also ggf. nur die Renderreihenfolge setzen.
Zustandsverwaltung und -steuerung
Die Verwaltung des Zustandes von Tools obliegt dem ToolManager. Der "Zustandsraum" von Tools umfasst dabei folgende Freiheitsgrade:
nicht registriert/registriert
Tools, die (noch) nicht bei einem bestimmten ToolManager registriert wurden, können in keiner Weise mit ihrem View interagieren. Erst durch die Registrierung werden sie dazu in die Lage versetzt. Bei der Registrierung wird die Methode setToolManager(ToolManager) des Tools aufgerufen.
aktiv/inaktiv
Bereits bei der Registrierung eines Tools kann angegeben werden, ob das Tool danach aktiv oder inaktiv sein soll. Inaktive Tools sind dem ToolManager zwar bekannt, sie erhalten aber weder Ereignisse noch die Gelegenheit zum Rendering. Ob ein Tool aktiv ist, kann durch die Methode setEnabled(Class<? extends Tool>, boolean) gesteuert werden.
exklusiv
Es kann sinnvoll sein, bestimmten Tools für einen begrenzten Zeitraum die alleinige Kontrolle zu überlassen, um Konflikte mit anderen Tools während komplexer Operationen zu vermeiden. Diese Exklusivität greift z.B. bei der Annotationsbearbeitung, während Annotationen in der Größe verändert oder verschoben werden, sowie während der Texteditor aktiv ist. Der Exklusivmodus sorgt dafür, dass lediglich das exklusive Tool mit Ereignissen versorgt wird und Gelegenheit zum Rendering erhält. In der Regel ist es das Tool selbst, das den Exklusivmodus mittels ToolManager.setExclusive(Class<? extends Tool>) aktiviert. Bei der Verwendung des Exklusivmodus ist es entscheidend, dafür zu sorgen, dass dieser auch zuverlässig wieder zu verlassen wird, da sonst ggf. die Funktion aller anderen Tools dauerhaft blockiert wird.
Der ToolManager
Einen Teil der Funktionen des ToolManagers haben wir oben bereits kennen gelernt, insbesondere diejenigen aus Sicht des Tools. Für die Integration stellt sich aber zunächst natürlich die Frage, wie auf ToolManager überhaupt zugegriffen werden kann, und welche Funktionen er bereitstellt.
ToolManager leben immer in enger Symbiose mit einer ViewComponent: jeder ToolManager hat immer genau eine ViewComponent und jede ViewComponent hat immer einen ToolManager. Derzeit existieren zwei konkrete Implementierungen von ViewComponent: PageView und ThumbnailView. Der ToolManager einer ViewComponent kann mit ViewComponent.getToolManager() erfragt werden. Einige der häufigsten Idiome bei der Arbeit mit dem ToolManager sind im folgenden Code-Beispiel aufgeführt.
ViewComponent viewComponent = ...;
// Registrieren eines Tools
viewComponent.getToolManager().register(MyTool.class, true);
// Erfragen und Konfigurieren eines Tools
if(viewComponent.getToolManager().hasTool(MyTool.class))
viewComponent.getToolManager().getTool(MyTool.class).setSomeOption(true);
// Aktivieren/Deaktivieren eines Tools
viewComponent.getToolManager().setEnabled(myTool.class, enabled);
// Deregistrieren eines Tools
viewComponent.getToolManager().deregister(MyTool.class);
Grau ist alle Theorie. In der nächste Folge werden wir deshalb zeigen, wie mit der Tool-API ohne viel Aufwand eigene Tools implementiert werden können.