Training „git“
07.10.2010 - 08.10.2010, Essen
Training „Eclipse RCP“
28.03.2011 - 01.04.2011, Dortmund
Eclipse RCP

Editoren in Eclipse RCP

In Eclipse RCP-Anwendungen werden Dokumente, die vom Benutzer geöffnet, bearbeitet und wieder gespeichert werden können, mit Editoren abgebildet. Dies bedingt weder eine Datei im Dateisystem noch dass es sich dabei um einen Texteditor handeln muss - die Texteditoren der Eclipse IDE sind lediglich eine spezielle Variante eines Editors. Ein Editor könnte beispielsweise eine Eingabemaske oder eine Grafik zum Gegenstand haben. Die Daten könnten in einer Datenbank persistiert werden. Dies obliegt dem Entwickler bei der Implementierung des Editors.

Alle Editoren werden im Editierbereich der Anwendung geöffnet. Diesen Bereich teilen sich alle Perspektiven, er kann lediglich ein- und ausgeblendet werden:

Editierbereich / Editor Area

EditorInput

Ein EditorInput-Objekt beschreibt eine Referenz auf die Daten, die der Editor bearbeitet. Bei einer Datei im Dateisystem würde das EditorInput-Objekt den Dateinamen beinhalten, bei einem Datenbankobjekt den Primärschlüssel. Für dieses Objekt ist das Interface IEditorInput zu implementieren:

Für einen Editor, dem ein Datenbank-Objekt SomeObject zugrunde liegt, könnte das Editor-Input-Objekt folgendermaßen implementiert werden:

/**
 * SomeEditorInput beschreibt eine Referenz auf SomeObject
 */
public class SomeEditorInput implements IEditorInput {

    // Minimale Informationen, mit der das zu bearbeitende Objekt beschrieben
    // und referenziert werden kann
    private int id;

    public SomeEditorInput(int id) {
        super();
        this.id = id;
    }

    public ImageDescriptor getImageDescriptor() {
        return Activator.getImageDescriptor("/icons/someicon.png");
    }

    public String getName() {
        // Text für den Benutzer, wird initial im Reiter des Editors angezeigt
        return "Some Document " + id;
    }

    public String getToolTipText() {
        return getName();
    }

    public IPersistableElement getPersistable() {
        // getPersistable muss nur implementiert werden, wenn das Editor-Inputobjekt
        // über mehrere Anwendungssessions hinweg gelten soll, z.B. wenn eine
        // "Zuletzt geöffnete Dokumente"-Liste verwendet wird.
        return null;
    }

    public boolean exists() {
        // Ggf. prüfen ob Objekt noch existiert und false zurückgeben wenn nicht.
        // Vor allem relevant im Zusammenspiel mit getPersistable.
        return true;
    }

    public Object getAdapter(Class adapterTarget) {
        // Editor-Input-Objekte können optional adaptierbar gestaltet werden
        return null;
    }

    // equals und hashCode müssen implementiert werden, damit nur ein
    // Editor für dasselbe Dokumente geöffnet werden kann

    public int hashCode() {
        return id;
    }

    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null || getClass() != obj.getClass())
            return false;
        return (someObjectId == ((SomeEditorInput) obj).id)
    }

}

Editor

Editoren werden der Workbench über den Extension Point org.eclipse.ui.editors bekannt gegeben:

<extension point="org.eclipse.ui.editors">
    <editor
        id="com.example.SomeEditorPart"
        class="com.example.SomeEditorPart"
        name="Some Object Editor">
    </editor>
</extension>

Hier ist die implementierende Klasse für den Editor anzugeben. Diese muss von EditorPart erben:

Der Editor ist so zu implementieren, dass die Daten anhand des EditorInput-Objektes geladen werden und entsprechend angezeigt werden. Beispielsweise:

public class SomeEditorPart extends EditorPart {

    public void init(IEditorSite site, IEditorInput input) throws PartInitException {
        // Sobald der Editor erzeugt wird, wird die init-Methode aufgerufen.
        // EditorPart verlangt, dass man die hereingereichte EditorSite und das
        // EditorInput-Objekt setzt. In dieser Methode könnte man auch
        // Nicht-UI bezogene Initialisierungen erledigen.
        setSite(site);
        setInput(input);

        // Sicherstellen, das das Input-Objekt vom erwarteten Typ ist
        Assert.isTrue(input instanceof SomeEditorInput,
                "Input object needs to be SomeEditorInput!");
    }

    public void createPartControl(Composite parent) {
        // Analog zu ViewParts sind in createPartControl die UI-Inhalte des
        // Editors zu erzeugen
        // ...
        
        // Titel des Editors kann gesetzt werden mit:
        setPartName(/* ... */);
    }

    public void setFocus() {
        // setFocus sollte in Editoren unbedingt implementiert werden und dem
        // ersten Control den Eingabefokus geben
    }

    public boolean isDirty() {
        // Die Workbench erledigt das Handling des Speichern und die
        // Anzeige des aktuellen Änderungsstatus des Editors. Dazu muss isDirty
        // Auskunft darüber geben, ob der Editor ungespeicherte Änderungen enthält
        // (= dirty ist). Dies ist gemäß der Inhalte des Editors herauszufinden und
        // zurückzugeben.
        return /* ... */;
    }

    private void setDirty(boolean dirty) {
        this.dirty = dirty;
        firePropertyChange(IEditorPart.PROP_DIRTY);
    }

    public void doSave(IProgressMonitor monitor) {
        // doSave wird aufgerufen, sobald der Benutzer den Speichern-Command
        // auslöst und sollte die Änderungen des Editors persistieren.
    }

    public void doSaveAs() {
        throw new UnsupportedOperationException();
    }

    public boolean isSaveAsAllowed() {
        return false;
    }
    
    // TIPP: getEditorInput überschreiben und ein konkretisiertes
    // EditorInput-Objekt zurückliefern
    public SomeEditorInput getEditorInput() {
        return (SomeEditorInput) super.getEditorInput();
    }
}

Speichern der Editor-Inhalte

Die Methode isDirty ist so zu implementieren, dass sie jederzeit Auskunft darüber gibt, ob ein Editor ungespeicherte Änderungen enthält. Damit dieser Status korrekt angezeigt werden kann, muss eine Benachrichtung erfolgen, sobald sich dieser dirty-Status ändert. Dies kann z.B. im Zusammenspiel mit den bearbeitenden UI-Elementen erfolgen:

public class SomeEditorPart extends EditorPart {

    private boolean dirty;

    public void createPartControl(Composite parent) {
        // ...
        someTextField.addModifyListener(new ModifyListener() {
            public void modifyText(ModifyEvent e) {
                setDirty(true);
            }
        });
    }

    public boolean isDirty() {
        return dirty;
    }

    public void setDirty(boolean dirty) {
        this.dirty = dirty;
        firePropertyChange(IEditorPart.PROP_DIRTY);
    }
    
}

Die Workbench erledigt dabei auch die Sicherheitsabfragen, sobald der Benutzer einen Editor mit ungespeicherten Änderungen schließen möchte:

Das Speichern wird über den Command org.eclipse.ui.file.save ausgelöst. Durch den Standard-Handler für den save-Command wird die Methode doSave des Editors aufgerufen.

Editoren öffnen, Workbench APIs für Editoren

Um einen Editor zu öffnen, rufen Sie openEditor() auf der Workbench-Page mit dem Editor-Input-Objekt und der Editor-ID auf:

// Der Zugriff auf Workbench-Page ist situationsabhängig
// Variante 1) in Views / Editoren
page = getSite().getPage();
// Variante 2) in Command-Handlern
page = HandlerUtil.getActiveWorkbenchWindow(event).getActivePage();
// Variante 3) globaler Zugriff
page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();

// Editor öffnen:
page.openEditor(new SomeEditorInput(someId), SOME_EDITOR_ID);

Weitere Methoden für das Handling von Editoren finden Sie ebenfalls auf der Workbench-Page:

// Alle Editoren schließen, optional mit Bestätigung bei ungespeicherten Änderungen
page.closeAllEditors(true);

// Editoren suchen
IEditorReference[] editorRefs = page.findEditors(someEditorInput, TestEditor.EDITOR_ID,
        IWorkbenchPage.MATCH_INPUT | IWorkbenchPage.MATCH_ID)
        
// Alle Editoren
IEditorReference[] editorRefs = page.getEditorReferences();

// Editoren schließen, optional mit Bestätigung bei ungespeicherten Änderungen
page.closeEditors(editorRefs);
Anonym, 06. Mai, 20:20 Uhr

Sehr gutes Tutorial.
Vielen Dank!

D.Zander, 12. Mai, 11:57 Uhr

Sie sagten, dass an ein EditorInput nur minimale Informationen übergeben werden sollten, mit deren Hilfe das referenzierte Objekt ermittelt werden kann. Das klingt auch sehr plausibel. Nur, um bei dem Datenbank-Beispiel zu bleiben, wann und wo instanziiere ich dieses Objekt?
Wenn ich bspw. im EditorInput in der getName()-Methode Informationen aus dem Datenbank-Objekt benötige, macht es dann nicht Sinn dieses Objekt anstatt der ID zu übergeben? Oder sollte es im EditorInput selbst geladen werden? Haben Sie dafür "Best Practices"?

Thomas Papendieck, 18. Mai, 10:32 Uhr

doSave() erhält einen IProgressMonitor.
Wie gehe ich damit um? Wird von Eclipse selbst protokolliert wieviele Editoren (noch) zu speichern sind oder muss ich diesen Object selbst aktualisieren?

mfG
Thomas

Ralf Ebert, 18. Mai, 16:30 Uhr

doSave() wird von der Workbench aufgerufen, um einen einzelnen Editor zu speichern: andere ggf. geöffnete Editoren spielen hier keine Rolle. Auf dem IProgressMonitor kann soweit bekannt der Fortschritt beim Speichern des einzelnen Editors mitgeteilt werden, siehe dazu auch das Kapitel zu den Eclipse Jobs.

Ralf Ebert, 18. Mai, 19:38 Uhr

@D.Zander, das Laden kann in der EditorPart-Klasse geschehen, man kann alternativ auch die IEditorInput#adapt Methode so implementieren, dass der Aufruf mit der Klasse des Modellobjektes die Daten lädt. Wenn das komplexer wird (z.B. clientseitiges Caching) empfiehlt sich ein clientseitiger Service, der diese Aufgaben mit erledigt.

Helmut Neubauer, 14. Juli, 15:01 Uhr

Das Tutorial ist sehr gelungen. Ich vermisse in dem Zusammenhang aber noch wie man in der RCP die zuletzt geöffneten Dateien im Menü anzeigen kann (à la Eclipse IDE). Ich habe gelesen, dass es einen Mechanismus diesbezüglich geben muss, mir ist aber nicht klar, wie ich das umzusetzen habe. Insbesondere wenn man z.B. zwei verschiedene Editoren hat und beispielsweise die jeweils zuletzt geöffneten Dateien in ebenfalls zwei Menüs anzeigen möchte. Diesen Fall habe ich aktuell konkret. Vielleicht können Sie da weiterhelfen.

Mathias, 26. Juli, 13:00 Uhr

Hallo Ralf
Prima Tutorial, das mir beim Verständnis von Editoren weiterhilft. In meiner Anwendung möchte ich einen Eclipse Form Multi-Page Editor einsetzen (Klasse FormEditor). Auf verschiedenen Pages (Klasse FormPage) bringe ich Textfelder mit Inhalten meines Modells zur Ansicht. Die Pages füge ich mit addPages() zum Editor hinzu. Leider kriege ich es nicht hin, dass mein Editor "dirty" wird, wenn ein Textfeld-Inhalt ändert. Ich bin mit diversen Versuchen hinsichtlich "Managed Forms" (Klasse SectionPart) gescheitert, weil ich einfach kein durchgängiges Beispiel zum Thema finden kann. Kannst Du Dir vorstellen, dazu ein Kapitel in Dein Buch aufzunehmen? Für sofortige Hinweise bin ich natürlich auch dankbar!
Gruss Mathias

Über Ihre Kommentare und Hinweise freue ich mich sehr:
Ralf Ebert | Eclipse RCP Buch | Editoren in Eclipse RCP