GeoCachingFrogger Teil 4 - Nodes und ExplorerManager

GeoCachingFrogger Teil 4 - Nodes und ExplorerManager

Einleitung

Als leidenschaftlicher Geocacher kommt man an einem Punkt an, an dem man gerne komfortabel die Geocaches verwalten und planen möchte. Es gibt dafür einige Tools im Internet, aber ich habe mir überlegt ein eigenes Tool zu schreiben und dabei die NetbeansRCP kennen zu lernen. Ich habe schon einige Swing-Anwendungen entwickelt, aber noch nie mit NetbeansRCP und bin gespannt, was mir die Plattform alles bietet und ob es tatsächlich der einfachere/bessere Weg ist.

Teil4 – Nodes und ExplorerManager

ModelManager

Wie im vorherigen Teil gezeigt, haben wir ein Model Modul in dem das Domain Model abgebildet wird und ein GPX Modul, welches die Daten aus eine GPX-Datei liest und das Domain Model aufbaut. Zur internen Verwaltung des Models habe ich einen Service “ModelManager” eingeführt. Dieser Manager verwaltet die Liste mit Caches.

package de.billmann.geocaching.model;

import java.util.Comparator;  
import java.util.List;

/**
 * @author abi
 */
public interface ModelManager {

    public void setCacheList(List list);

    public List getCacheList();

    public void addCacheToList(Cache cache);

    public void addCachesToList(List list);

    public List getCacheList(Comparator comparator);

    public void addModelChangeListener(ModelChangedListener listener);

    public void removeModelChangeListener(ModelChangedListener listener);
}

Wie der Manager die Caches verwaltet ist der Anwendung egal. Der UI-Teil kennt dank der angegebenen Abhängigkeiten (siehe Teil3) nur das Interface. Die Implementierung ist in einem nicht öffentlichen Package. So lässt sich die Implementierung zu einem späteren Zeitpunkt tauschen.

package de.billmann.geocaching.model.impl;

import de.billmann.geocaching.model.Cache;  
import de.billmann.geocaching.model.ModelChangedListener;  
import de.billmann.geocaching.model.ModelEvent;  
import de.billmann.geocaching.model.ModelManager;  
import java.util.ArrayList;  
import java.util.Collections;  
import java.util.Comparator;  
import java.util.List;  
import org.openide.util.lookup.ServiceProvider;

/**
 *
 * @author abi
 */
@ServiceProvider(service=ModelManager.class)
public class ModelManagerImpl implements ModelManager {

    private List list = new ArrayList();
    private List listeners = new ArrayList();

    public void setCacheList(List list) {
        this.list.clear();
        this.list.addAll(list);
        this.fireModelChangeEvent();
    }

    public List getCacheList() {
        return new ArrayList(this.list);
    }

    public void addCacheToList(Cache cache) {
        this.list.add(cache);
        this.fireModelChangeEvent();
    }

    public void addCachesToList(List list) {
        this.list.addAll(list);
        this.fireModelChangeEvent();
    }

    public List getCacheList(Comparator comparator) {
        List list = new ArrayList(this.list);
        Collections.sort(list, comparator);
        return list;
    }

    public void addModelChangeListener(ModelChangedListener listener) {
        if (!this.listeners.contains(listener)) {
            this.listeners.add(listener);
        }
    }

    public void removeModelChangeListener(ModelChangedListener listener) {
        if (this.listeners.contains(listener)) {
            this.listeners.remove(listener);
        }
    }

    public void fireModelChangeEvent() {
        final ModelEvent event = new ModelEvent(this);
        for (ModelChangedListener listener: listeners) {
            listener.modelChanged(event);
        }
    }

}

Am ModelManager kann man einen ModelChangedListener registrieren, so dass alle interessierten Teile der Anwendung über Änderungen informiert werden.

ImportGpxAction

Um das Einlesen einer GPX-Datei anzustossen, benötigen wir eine Action. Diese Action öffnet im einfachsten Fall eine direkt angegebene Datei, in der späteren Version wird da noch ein FileChooserDialog benutzt.

package de.billmann.geocaching.frogger.actions;

import de.billmann.geocaching.gpx.GPXReader;  
import de.billmann.geocaching.model.Cache;  
import de.billmann.geocaching.model.ModelManager;  
import java.awt.event.ActionEvent;  
import java.awt.event.ActionListener;  
import java.io.File;  
import java.io.FileInputStream;  
import java.io.FileNotFoundException;  
import java.io.InputStream;  
import java.util.Collection;  
import java.util.List;  
import java.util.concurrent.ExecutionException;  
import javax.swing.SwingWorker;  
import javax.swing.filechooser.FileFilter;  
import org.openide.awt.ActionID;  
import org.openide.awt.ActionReference;  
import org.openide.awt.ActionReferences;  
import org.openide.awt.ActionRegistration;  
import org.openide.filesystems.FileChooserBuilder;  
import org.openide.util.Exceptions;  
import org.openide.util.Lookup;  
import org.openide.util.NbBundle.Messages;  
import org.openide.windows.IOProvider;  
import org.openide.windows.InputOutput;

@ActionID(category = "File",
id = "de.billmann.geocaching.frogger.actions.ImportGpxAction")  
@ActionRegistration(iconBase = "de/billmann/geocaching/frogger/actions/gpxopen.png",
displayName = "#CTL_ImportGpxAction")  
@ActionReferences({
    @ActionReference(path = "Menu/File", position = 1300),
    @ActionReference(path = "Toolbars/File", position = 300),
    @ActionReference(path = "Shortcuts", name = "D-O")
})
@Messages("CTL_ImportGpxAction=Import GPX")
public final class ImportGpxAction implements ActionListener {

    public void actionPerformed(ActionEvent e) {
        final File file = new File("PATH_TO_TESTFILE");

        if ( file != null ) {
            // Construct a new SwingWorker
            SwingWorker, Void> worker = new SwingWorker, Void>(){
                Collection allFilters = Lookup.getDefault().lookupAll(GPXReader.class);

                @Override
                protected List doInBackground(){
                    List cacheList = null;
                    try {
                        InputStream in = new FileInputStream(file);

                        for (GPXReader reader : allFilters) {
                            cacheList = reader.importGPX(in);
                        }            
                    } catch (FileNotFoundException ex) {
                    }
                    return cacheList;
                }

                @Override
                protected void done(){
                    try {
                        final List list = get();
                        final ModelManager modelManager = Lookup.getDefault().lookup(ModelManager.class);
                        modelManager.addCachesToList(list);
                    } catch (InterruptedException e) {
                    } catch (ExecutionException e) {
                    }
                }
            };
            // Execute the SwingWorker; the GUI will not freeze
            worker.execute();                
        }
    }
}

Damit die UI nicht blockiert wird, benutzen wir an dieser Stelle den SwingWorker, der die Arbeit in einem eigenen Worker-Thread verrichtet. Man sieht, dass die Action irgendeine Implementierung des GPXReader-Interfaces aus dem Lookup holt (auch hier sind dann später verschiedene Implementierungen denkbar). Nachdem die komplette Datei eingelesen wurde, wird das Ergebnis (eine Liste von Caches) an den ModelManager übergeben, damit dieser dann ab diesem Zeitpunkt verwaltet.

Nodes

Für die Darstellung in den Baumkomponenten von NetBeans werden Nodes verwendet. Diese liefern den Komponenten die zur Darstellung benötigten Daten. Wir verwenden eine Ableitung der BeanNode, welche einen Cache mitbekommt, aus welchem die Werte gelesen werden.

package de.billmann.geocaching.frogger.uimodel;

import de.billmann.geocaching.frogger.actions.OpenCacheCookie;  
import de.billmann.geocaching.model.Cache;  
import java.awt.Image;  
import java.beans.IntrospectionException;  
import javax.swing.Action;  
import org.openide.actions.OpenAction;  
import org.openide.nodes.BeanNode;  
import org.openide.nodes.Children;  
import org.openide.util.ImageUtilities;  
import org.openide.util.actions.SystemAction;  
import org.openide.util.lookup.Lookups;

/**
 *
 * @author abi
 */
public class CacheNode extends BeanNode {

    public CacheNode(final Cache cache) throws IntrospectionException {
        super(cache, Children.LEAF, Lookups.fixed(cache, new OpenCacheCookie(cache)));
        this.setName(cache.getName());
    }  

    /** Providing the Open action on a feed entry */
    @Override
    public Action[] getActions(boolean popup) {
        return new Action[]{SystemAction.get(OpenAction.class)};
    }

    @Override
    public Action getPreferredAction() {
        return getActions(false)[0];
    }    

    @Override
    public Image getIcon (int type) {
        if (this.getBean() != null) {
            if (this.getBean().getType() != null) {
                final Image image = ImageUtilities.loadImage ("de/billmann/geocaching/frogger/uimodel/"+this.getBean().getType().toString().toLowerCase()+".png");
                if (image != null) {
                    return image;
                }
            }
        }

        return super.getIcon(type);
    }    
}

Für das Erzeugen der CacheNodes ist eine Factory nötig. Diese Factory registriert sich an dem ModelManager als Listener und ab sofort wird immer, wenn der ModelManager eine Veränderung in der Liste meldet, ein refresh ausgeführt, was dazu führt, dass der komplette Baum neu erzeugt wird.

package de.billmann.geocaching.frogger.uimodel;

import de.billmann.geocaching.model.Cache;  
import de.billmann.geocaching.model.ModelChangedListener;  
import de.billmann.geocaching.model.ModelEvent;  
import de.billmann.geocaching.model.ModelManager;  
import java.beans.IntrospectionException;  
import java.util.List;  
import org.openide.nodes.ChildFactory;  
import org.openide.nodes.Node;  
import org.openide.util.Exceptions;  
import org.openide.util.Lookup;

/**
 *
 * @author abi
 */
public class CacheChildFactory extends ChildFactory implements ModelChangedListener {

    private ModelManager modelManager = null;

    public CacheChildFactory() {
        modelManager = Lookup.getDefault().lookup(ModelManager.class);
        modelManager.addModelChangeListener(this);
    }

    @Override
    protected boolean createKeys(List list) {
        final List cacheList = modelManager.getCacheList();
        for (Cache cache: cacheList) {
            list.add(cache);
        }
        return true;
    }

    @Override
    protected Node createNodeForKey(Object key) {
        final List cacheList = modelManager.getCacheList();
        Node node = null;
        for (Cache cache: cacheList) {
            if (cache.equals(key)) {
                try {
                    node = new CacheNode(cache);
                } catch (IntrospectionException ex) {
                    Exceptions.printStackTrace(ex);
                }
                break;
            }
        }
        return node;
    }

    @Override
    public void modelChanged(ModelEvent event) {
        this.refresh(true);
    }

}

CacheListTopComponent

Jetzt kommen wir aber zur eigentlichen Darstellung. Hierzu erstellen wir uns eine Window Component mit dem Wizard von NetBeans.

Daraus entsteht nun eine Form mit zugehörigem Code, die wir für die Baumdarstellung nutzen können.

Um die Baumdarstellung zu unterstützen muss unsere Komponente das Explorer.Provider-Interface implementieren. Hier muss die Methode getExplorerManager implementiert werden.

private final ExplorerManager mgr = new ExplorerManager();

    @Override
    public ExplorerManager getExplorerManager() {
        return mgr;
    }

Nun muss man nur noch den ExplorerManager (den man einfach instanziiert) irgendwie mit der CacheChildFactory verbandeln. Dies geschieht im Konstruktor der TopComponent. Da uns für die Baumhierarchie der Root-Knoten fehlt,  erzeugen wir einen Dummy-Knoten mit dem Namen “Caches” unter dem dann unsere Caches hängen werden.

package de.billmann.geocaching.frogger;

import de.billmann.geocaching.frogger.uimodel.CacheChildFactory;  
import org.netbeans.api.settings.ConvertAsProperties;  
import org.openide.awt.ActionID;  
import org.openide.awt.ActionReference;  
import org.openide.explorer.ExplorerManager;  
import org.openide.explorer.ExplorerUtils;  
import org.openide.explorer.view.OutlineView;  
import org.openide.nodes.AbstractNode;  
import org.openide.nodes.Children;  
import org.openide.nodes.Node;  
import org.openide.util.NbBundle;  
import org.openide.windows.TopComponent;

/**
 * Top component which displays something.
 */
@ConvertAsProperties(
    dtd="-//de.billmann.geocaching.frogger//CacheList//EN",
    autostore=false
)
@TopComponent.Description(
    preferredID="CacheListTopComponent", 
    //iconBase="SET/PATH/TO/ICON/HERE", 
    persistenceType=TopComponent.PERSISTENCE_ALWAYS
)
@TopComponent.Registration(mode = "explorer", openAtStartup = true)
@ActionID(category = "Window", id = "de.billmann.geocaching.frogger.CacheListTopComponent")
@ActionReference(path = "Menu/Window" /*, position = 333 */)
@TopComponent.OpenActionRegistration(
    displayName = "#CTL_CacheListAction",
    preferredID="CacheListTopComponent"
)
public final class CacheListTopComponent extends TopComponent implements ExplorerManager.Provider {  
    public CacheListTopComponent() {
        initComponents();
        setName(NbBundle.getMessage(CacheListTopComponent.class, "CTL_CacheListTopComponent"));
        setToolTipText(NbBundle.getMessage(CacheListTopComponent.class, "HINT_CacheListTopComponent"));
        final Children children = Children.create(new CacheChildFactory(), true);
        final Node root = new AbstractNode(children);
        root.setDisplayName("Caches");
        mgr.setRootContext(root);
        associateLookup(ExplorerUtils.createLookup(mgr, getActionMap()));
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    //                           
    private void initComponents() {

        jScrollPane1 = new OutlineView();

        setLayout(new java.awt.BorderLayout());
        add(jScrollPane1, java.awt.BorderLayout.CENTER);
    }//                         

    // Variables declaration - do not modify                     
    private javax.swing.JScrollPane jScrollPane1;
    // End of variables declaration                   

    @Override
    public void componentOpened() {
        // TODO add custom code on component opening
    }

    @Override
    public void componentClosed() {
        // TODO add custom code on component closing
    }

    void writeProperties(java.util.Properties p) {
        // better to version settings since initial version as advocated at
        // http://wiki.apidesign.org/wiki/PropertyFiles
        p.setProperty("version", "1.0");
        // TODO store your settings
    }
    void readProperties(java.util.Properties p) {
        String version = p.getProperty("version");
        // TODO read your settings according to their version
    }

    private final ExplorerManager mgr = new ExplorerManager();

    @Override
    public ExplorerManager getExplorerManager() {
        return mgr;
    }

}

Nun können wir die Caches einlesen und in unserem Baum darstellen.

Fazit

Es sind schon einige Schritte notwendig um an diese Stelle zu kommen und es erscheint einem nicht immer ganz sinnvoll, wieso man so häufig einen Umweg über die Lookups machen sollte, aber gerade wenn man später mehrere Implementierungen anbieten möchte, dann entkoppelt man damit schön die einzelnen Teile der Software. Im nächsten Teil wird bei einem Doppelklick auf einen Cache ein Editor geöffnet.

Comments