September 30, 2009

NetBeans Platform: Implement Perforce client - part III

Switch to P4JAPI, annotate and basic actions

All the Netbeans Plugins which tried to implement the Perforce client uses p4 command line tool. This is somehow difficult, because you have to manage the input and output of a command line. A lot of effort with no benefit. Because the perforce team put on place what is called P4Java, the Perforce Java API. You can find the release notes here (last version) http://www.perforce.com/perforce/doc.091/user/p4javanotes.txt. Download it from here. The main benefits are the fact that you work with objects, don't have to parse things, and to manage external processes. Thought, also, it is faster than its command line brother. Since this is not an NetBeans Platform thing, I will not insist on that too much.

As mentioned in a previous article, the presentation of versioning information is implemented in a class based on VCSAnnotator. In our case, this class is called PerforceAnnotator. There are some aspects which need to be explained. First is the fact that this class is a listener on some specific property change. That is done using the following code:

public class PerforceAnnotator extends VCSAnnotator {
    /**
     * Fired when textual annotations and badges have changed. 
     * The NEW value is Set<File> of files that changed or NULL
     * if all annotaions changed.
     */
    public static final String PROP_ANNOTATIONS_CHANGED = "annotationsChanged";
    private final PropertyChangeSupport support = new PropertyChangeSupport(this);
     public void addPropertyChangeListener(PropertyChangeListener listener) {
        support.addPropertyChangeListener(listener);
    }

    public void removePropertyChangeListener(PropertyChangeListener listener) {
        support.removePropertyChangeListener(listener);
    } 


The PerforceAnnotator has two methods related to property change events: addPropertyChangeListener and removePropertyChangeListener. How it works? We define a logical property, named "annotationChanged". Any listener added will be notified if this property have been changed. This code works together with another piece from PerforceVS and another one from FileStatusCache. Practically, through this logical properties and wire of listeners we send messages from a component to another. They remain loosely coupled. As a sample, when the status of a file has been changed (from perforce point of view), the instance of FileStatusCache (which manages these statuses) notify PerforceVS that something on a file has been changed. PerforceVS than will notify the PerforceAnnotator about that change and the annotator will change also the status in UI (labels, colors, versioning info). The property change method is a very important concept. It is used very ofted to put UI things together, so take a closer lok on that, whenever you have some time.

PerforceAnnotator show some UI information about the status of files. The icon from explorer view is modified and the name and color of the file element is modified. These is done with the following code.

    @Override
    public Image annotateIcon(Image oldImage, VCSContext context) {
        FileStatusCache cache = PerforceSystem.getCache();
        int x = 12;
        int y = 0;
        if (cache.containsStatus(context, FileStatus.STATUS_VERSIONED_EDIT)) {
            return ImageUtilities.mergeImages(oldImage, ImageRoot.DECORATION_EDIT, x, y);
        }
        if(cache.containsStatus(context, FileStatus.STATUS_VERSIONED_ADD)) {
            return ImageUtilities.mergeImages(oldImage, ImageRoot.DECORATION_ADD, x, y);
        }
        if(cache.containsStatus(context, FileStatus.STATUS_VERSIONED_UPTODATE)) {
            return super.annotateIcon(oldImage, context);
        }
        return super.annotateIcon(oldImage, context);
    }

    @Override
    public String annotateName(String name, VCSContext ctx) {
        FileStatusCache cache = PerforceSystem.getCache();
        int version;
        List<FileInformation> files;
        files = cache.listFiles(ctx, FileStatus.STATUS_VERSIONED_EDIT);
        if (files.size() > 0) {
            version = files.get(0).getVersion();
            return markName(name, "#0B610B", "#" + version + " [edit]");
        }
        files = cache.listFiles(ctx, FileStatus.STATUS_VERSIONED_ADD);
        if (files.size() > 0) {
            version = files.get(0).getVersion();
            return markName(name, "#084B8A", "#" + version + " [add]");
        }
        files = cache.listFiles(ctx, FileStatus.STATUS_VERSIONED_UPTODATE);
        if (files.size() > 0) {
            version = files.get(0).getVersion();
            return markName(name, null, "#" + version);
        }
        files = cache.listFiles(ctx, FileStatus.STATUS_UNKNOWN);
        if (files.size() > 0) {
            return super.annotateName(name, ctx);
        }
        if (name.equalsIgnoreCase("<default package>")) {
            return "&lt;default package&gt;";
        }
        return super.annotateName(name, ctx);
    }

    private String markName(String name, String color, String label) {
        boolean extra = VersioningSupport.getPreferences().getBoolean(
                VersioningSupport.PREF_BOOLEAN_TEXT_ANNOTATIONS_VISIBLE, false);

        if (color == null) {
            return name + (extra ? label : "");
        }
        return "<font color=\"" + color + "\">" + name + "</font>" +
                (extra ? "  <font color=\"" + color + "\">" + label + "</font>" : "");
    }

How it works? Depending on the status of the file, we modify the actual icon or label of the file from explorer view. To find the status of the file we ask the FileStatusCache, this class manages the FileInformation related to every managed file. The VCSContext represents the selection of files on which we should show information. Note that is possible to have multiple files in a context. In order to keep things simple, I considered that context have only one file and if there are many, I take into consideration only the first one. In time this should be changed. The same scenario happens for labels. On labels we use HTML tags because the view allow that to modify the aspect of a label.

One small tip to know. ImageUtilities class offers some very useful methods in managing images. We can use that class to merge two images, as is the case with PerforceAnnotator (we put a small Perforce icon over the original NetBeans file icon). We can load an image giving only a location in class path and we can transform with easy from an image into an icon and viceversa.

Let's take a look on the results of our work until now.


Another thing which PerfoceAnnotator do is to wire up some actions. It does this by implementing the method getActions. This method receives two parameters as input. The first one is the VCSContext. As noted before, the context gives information about the selected files for which the user wants to show actions. I repeat that is important to be aware that the context can represent more than one file, so behave accordingly. The second parameter is ActionDestination. This is an enum which tells us if the IDE wants the actions to be inserted on the main menu or on the contextual menu. In the sample code below the complexity of building available actions is wrapped into another class, called FileStatusManager. An action is a standard swing action. My actions usually calls the perforce client and do something with that. Since this is not a NetBeans Platform I will not talk about that, as usual, I invite you to take a look on the sources and give at least a feedback (you are welcomed anytime to submit).

    @Override
    public Action[] getActions(VCSContext ctx, ActionDestination destination) {
        List<Action> actions = new ArrayList<Action>();
        FileStatusManager manager = FileStatusManager.getInstance();
        switch (destination) {
            case MainMenu:
                actions.addAll(manager.getAvailableActionsOnMainMenu(ctx));
                break;
            case PopupMenu:
                actions.addAll(manager.getAvailableActionsOnPopup(ctx));
                break;
        }
        return actions.toArray(new Action[actions.size()]);
    }

One more cookie, thought. To create a progress notification which will be displayed on status bar in a dedicated section, you can use ProgressHandle. Like in the sample below taken form FileStatusCache.reloadCacheOnThread.

        final ProgressHandle progress =


                ProgressHandleFactory.createHandle("Perforce refresh..");


        progress.start();


        .....


        progress.finish();


              
I mention this simple thing because a lot of things in NetBeans are very simple and straightforward to implement or use. That is the meaning of  strong API. The simplest form possible, intuitive and elegant. I think NetBeans Platform has a lot of those.

Untill the next time, see changes on http://kenai.com/projects/perforcenb.

1 comment:

  1. I've been meaning to get around to making just such a plugin since the new p4 java api came out. I just haven't been able to find the time. I just looked through your source code. It looks great! I can't wait to try it out.

    ReplyDelete