Java Getriebe

Java und NetBeans

Projektbezogene Context-Action

In NetBeans RCP Anwendungen in denen mit Projekten gearbeitet wird, ist die „Project API“ und „Project UI API“ unverzichtbar. Sie bieten die komplette Funktionalität, die man von Projekten in der NetBeans IDE kennt. Erweiterbare Nodes, Konfigurationsverwaltung, Abhängigkeiten und vieles mehr. Eine Fähigkeit dieser großartigen API durfte ich heute neu entdecken.

NetBeans kennt so genannte ContextActions. Dies sind Actions, die zum Beispiel in der Toolbar angezeigt werden und sich selber aktivieren, sobald im Lookup des aktivierten Node eine bestimmte Klasseninstanz vorhanden ist. Welche Klasse als „Trigger“ verwendet wird kann für jede Action einzeln eingestellt werden. Prima Sache. So kann man ein Toolbar Icon immer nur dann aktivieren, wenn die eigene TopComponent aktiv ist oder der Node des eigenen Datenobjekts.

Arbeitet man jetzt jedoch mit Projekten und möchte eine Action immer dann aktivieren, wenn ein bestimmtes Projekt oder ein bestimmter Projekttyp aktiv ist, funktioniert dieser Ansatz leider nicht. Denn die „Project“ Instanz ist zwar im Lookup des Projektnodes selbst, aber nicht zwingend in den Kind-Nodes oder gar den entsprechenden TopComponents. In unserer RCP haben wir versucht diesen Missstand dadurch zu umgehen, dass wir versucht haben alle „notwendigen“ „Context-Trigger“ an die Kindnodes weiter zu reichen in dem wir einen entsprechenden FilterNode über das Projektnode gestülpt haben. In engen, definierten Grenzen hat dies sogar funktioniert. Aber ein Aufwand, den man gar nicht betreiben muss. Das geht viel einfacher. Die entsprechende Klasse findet sich bereits in der Projekt UI API: ProjectSensitiveActions

Wie der Name schon andeutet lassen sich mit dieser Factoryklasse Actions erstellen, die im Kontext des aktuellen Projekts agieren können. Dazu stehen zwei Varianten zur Auswahl: eine projectCommandAction und eine projectSensitiveAction. Beide Versionen delegieren die eigentliche Aufgabe zu Testen ob die Action aktiviert sein soll oder nicht an andere Objekte weiter. Damit muss man sich um die Eigenheiten einer Action gar nicht weiter kümmern sondern kann sich auf das wesentliche Konzentrieren.

Die einfachere Version ist sicherlich eine projectSensitiveAction. Hier benötigt man eine eigene Implementierung eines ProjectActionPerformer. Die beiden Methoden enable und perform erklären sich dabei von selbst. Beide bekommen das entsprechende Projekt der aktuellen Auswahl übergeben und können entsprechend handeln. Der Vorteil ist, dass genau wie bei einer ContextAction ein bestimmtes Objekt aus dem Lookup geprüft werden kann welches als Marker dient und die gewünschten Informationen bereit hält.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public boolean enable(Project project)
{
  SimulationFactory factory =
      project.getLookup().lookup(SimulationFactory.class);
  return factory != null;
}
@Override
public void perform(Project project)
{
  SimulationFactory factory =
      project.getLookup().lookup(SimulationFactory.class);

  Simulation simulation = factory.createSimulation(project);
  simulation.simulate();
}

Alle Projekte, die eine SimulationFactory in ihrem Lookup haben aktivieren automatisch diese Action.

Etwas mehr Aufwand, aber dafür mehr Möglichkeiten hat man mit einer projectCommandAction. Der große Vorteil dieser Action ist, dass man beim Erstellen der Actioninstanz selbst die eigentliche Implementierung der auszuführenden Aktion nicht kennen muss. Die Hilfsklasse dazu heißt ActionProvider und muss im Lookup des Projekts selbst platziert werden. Dafür hat man die Möglichkeit mehrere Instanzen davon im Lookup zu verstauen und jeder dieser Instanzen ein eigenes Kommando zu definieren. Eine ganze Reihe von Standardaktionen sind bereits in der Klasse als Konstanten definiert. Diese sind recht stark an die IDE und dessen typische Aufgaben angelehnt, aber das muss niemanden abhalten eigene Kommandos zu definieren. Mit diesen Kommandos kann man die Implementierungen auch ohne weiteres in andere Module auslagern und per Layereintäge in das Lookup des Projekts eintragen.

Das schönen an den ProjectSensitiveActions ist, dass sie nicht nur beim Projektnode selber funktioniert, sondern bei allen Kindknoten und TopComponents, die mindestens ein Project oder FileObject im Lookup haben. Bei letzerem wird einfach geschaut, zu welchem Projekt die Datei oder der Ordner gehört.

Aber…

Einen Wermutstropfen hat die ganze Sache allerdings. Sie funktioniert (zumindest vor NetBeans 7.2, siehe Buzilla#206093) nicht ganz intuitiv mit der @ActionRegistration zusammen. Die Annotation erstellt zum einen liebend gerne einen Wrapper, so dass die eigene Action erst dann erzeugt, wenn mindestens ein Mal auf die Action geklickt wurde und zum Anderen mag sie keine Methoden mit Argumenten.

Eine ActionRegistration kann man auch an einer statischen Methode anbringen. Diese benötigen wir in jedem Fall, da wir unsererseits die eigentliche Actioninstanz mit einem Aufruf einer Factorymethode erzeugen. Damit können wir keine Klasse markieren. Aber auch für statische Methoden erzeugt die ActionRegistration einen Wrapper. Dies können wir nur dadurch umgehen in dem wir eine ContextAwareAction zurück liefern. Zum Glück implementieren die Actions der ProjectSensitiveAction Methoden dieses Interface, so dass wir ohne weiteres

1
2
return (ContextAwareAction)ProjectSensitiveActions
        .projectSensitiveAction([...]);

schreiben können.

Dass die ActionRegistration keine Argumente bei den Methoden erlaubt ist deswegen schade, da der erzeugte Layereintrag mit den entsprechenden „methodvalue“ durchaus Methoden mit Argumenten erlaubt. In diesem Fall wäre die Variante mit „methode(Map map)“ von interesse, weil wir aus dieser Map die Informationen zu „displayName“ und „iconBase“ holen könnten. So müssen wir diese Informationen leider redundant ein zweites Mal in den Quelltext schreiben und nehmen und hier auch die Möglichkeit den Layereintrag in einem anderen Modul zu manipulieren.