November 25, 2009

NetBeans Platform: Output windows in simple words

I/O APIs is among the most used modules from NetBeans Platform. This is one of the most common way to show information about your activity. The name can be misleading, but is much simpler that it seems. This module manages the output windows which you already have or you can create, where text information can be put.
There are a a lot of scenarios where you need output windows. You need them to show logs for bootstrapping tasks, to show log information, to receive feedback from a compiler or builder or other type of tool.

The story about output windows in NetBeans can be simple or complicated. The hard way is to implement yourself everything and control everything. That could be feasible if you plan to implement a rich output windows with a lot of controls on it. But for simple scenarios, what you have from the platform is simple enough.

Setup

From your NetBeans Platform module, all you have to do is to add "I/O APIs" as a module dependency. That's all.

"Hello world!" from output window

IOProvider is a factory for output window components. These output windows are represented by InputOutput class and hosted into a container, represented by IOContainer. It's easy and enough to proceed. First, we ask for the default provider to get an InputOutput with a specific title. We can create a new one or get an existing one. After that we activate the output and write to it. Here's the code.

InputOutput io = IOProvider.getDefault().getIO("TestIO", true);
io.select();
io.getOut().print("Hello world!");

Put some colors and behavior on lines

We can easily color the line from output. Also we can put some action on the line. If we create a line with an listener (our listener), the line from output would behave as a hyperlink in a browser. When the listened line will be selected, clicked or deleted, we can do something through listener. Let's see some more code.

try {
    InputOutput io = IOProvider.getDefault().getIO("My title", false);
    IOColorLines.println(io, "This is a yellow line", Color.yellow);
    IOColorLines.println(io, "This is e red important line",
                    null, true, Color.red);
    IOColorLines.println(io, "one dynamic line with event",
        new LineListener(), true, Color.green);
} catch (IOException ex) {
    Logger.getLogger(SameIOAction.class.getName()).log(
        Level.SEVERE, null, ex);
}

[...]
class Listener implements OutputListener {
    public void outputLineSelected(OutputEvent ev) {
        JOptionPane.showMessageDialog(null, "line with content " +
                ev.getLine() + " was selected");
    }
    public void outputLineAction(OutputEvent ev) {
        JOptionPane.showMessageDialog(null, "line with content " +
                ev.getLine() + " was actioned");
    }
    public void outputLineCleared(OutputEvent ev) {
        JOptionPane.showMessageDialog(null, "line with content " +
                ev.getLine() + " was cleared");
    }
}

Put some decorations on output windows

Using another class from the package, IOTab, you can easily add a tooltip text and a small icon on an output window. As a sample:

InputOutput io = IOProvider.getDefault().getIO("My title", false);
IOTab.setToolTipText(io, "My tooltip text for Hello world!");
[..]


And you can do more..

There is also a simple way to add some Swing actions on output window. The output window has a small toolbar on the left side of it. If you pass an array of Swing Actions on the moment of creation, these actions will be available for use on your output window. Just to name a very often scenario: you have a long process to run, you want to give full text feedback, but you want to be able to stop the process when you think is appropriate. Just create a Swing Action to fire the canceling job and add it to the InputOutput.

Action[] actionList = new Action[5];
[..] // add actions here
InputOutput io = IOProvider.getDefault().getIO("My title", actionList);

Thought there are some limitations: you can't put here more than 5 actions and each action should have the property Action.SMALL_ICON defined.

Have fun with InputOutput windows!

November 3, 2009

NetBeans Platform: Implement Perforce client - part IV

Automatically add, checkout, delete or move files
This is the last article about the Perforce client. That does not mean that is fully implemented, niether I will abandon the project. The NetBeans Perforce client will continue to be developed. By me, and hopefully by others. The point with this string of articles it to illustrate in which way it can be implemented a versioning client in NetBeans.

Since last article, the Perforce client was enriched with many functionalities. Thought, only the last one is relevant for our purpose. The topic is how to integrate into Perforce the IDE file manipulation operations like add, delete, rename, move or edit. You can add here also, how to handle the same situations, when files were modified outside IDE.

The key to this functionality is a class called VCSInterceptor. This class is used by NetBeans to announce the eventually versioning system about some change over files. Either if the change was operated form inside or outside of IDE.

I will split the methods from VCSInterceptor into five categories: queries, delete, move, create and change. You can find the source (comments are very intereting to read) here. Take a close look there.

queries
This category contain only one method isMutable(File file). This method is used to ovveride the default behavior proposed by the versioning systems which uses read-only files. When a file is read only, the IDE will see the file in this state, so it will not edit it. But when this method returns true, you let the IDE know that you actually can edit the file, even if is marked as read-only from the file system.

delete
beforeDelete and doDelete work together. The first let the IDE know if you want to implement the delete action for the specified file, in the second method you actually write the delete operation. There is another one method, afterDelete. This method is called after the file was deleted from the files system. Besides the moment of the notification, there is another very important difference. All methods are called when a delete operation was realized from IDE, but only the later (afterDelete) is fired when a file was deleted outside IDE. Take this into consideration when you implement the delete handling.

move
beforeMove, doMove, afterMove. Follows the same pattern as for delete operation. beforeMove is used to tell to the IDE that is want or don't want to handle the file move. doMove implements the real move action and afterMove is called (hard to believe, but ) .. after the mov operation. The only difference from delete is that all operations are called only for IDE file rename/move opertions. For this kind of operations outside IDE, NetBeans will fire two events: afterDelete for the source and afterCreate for the target.

create
Following the same pattern, for create we have beforeCreate, doCreate and afterCreate. Adding new files from outside IDE will fire only the last event (like on delete operations). I will go on, I hate to repeat.

change
This breaks the pattern. There is an beforeEdit method called when a file is about to be opened in edit mode. The is a perfect moment to do a checkout if you are in IDE.  After that we have two events, beforeChange and afterChange. These events are related to content. beforeChange is called before the file content are about to be changed. afterChange end the cycle. One thing to mention here is that only afterChange is called if the file is updated outside IDE (javadoc does not mention that, but you can trust me on that). The later is a perfect place to put a checkout for outside IDE file modifications.

Just before presenting the source code for that, I have to mention that you don't need to implement all methods. These methods tries to catch all the possible events for all the possible versioning system scenarios. In my case i found to be enough to implement only 7 from 13. And I hope I covered all.

Here is the interceptor code:

public class PerforceInterceptor extends VCSInterceptor {

    @Override
    public boolean isMutable(File file) {
        return true; // really, for all? we will see that
    }

    /**
     * Automatically add to perforce the new added file.
     * The code is here to handle also the files added from outside IDE.
     *
     * @param file file in question
     */
    @Override
    public void afterCreate(File file) {
        if (PerforceModuleConfig.getInstance().isPerforceExcluded(file)) {
            return;
        }
        FileStatus status = getUpdatedFileStatus(file);
        if (status == null || FileStatus.STATUS_UNKNOWN.equals(status)) {
            try {
                // this is what we really care
                P4Client p4client = PerforceSystem.getP4Client();
                p4client.actionAdd(file);
            } catch (PerforceActionException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
    }

    /**
     * Called when a file is uptodate and is about to be modified.
     * The file is automatically checked out.
     *
     * @param file file in question
     */
    @Override
    public void beforeEdit(final File file) {
        if (PerforceModuleConfig.getInstance().isPerforceExcluded(file)) {
            return;
        }
        FileStatus status = getUpdatedFileStatus(file);
        if (!FileStatus.STATUS_VERSIONED_UPTODATE.equals(status)) {
            return;
        }
        try {
            P4Client p4client = PerforceSystem.getP4Client();
            p4client.actionEdit(file);
        } catch (PerforceActionException ex) {
            Exceptions.printStackTrace(ex);
        }
    }

    /**
     * Check out the uptodate file. The method is called when the file was
     * modified outside IDE.
     * Same logic as {@link #beforeEdit(java.io.File) }.
     *
     * @param file file in question
     */
    @Override
    public void afterChange(File file) {
        beforeEdit(file);
    }

    /**
     * Mark for delete files which are delete from outside/inside IDE.
     * Handle different scenarios depending on the status of the file.
     *
     * @param file file in question
     */
    @Override
    public void afterDelete(File file) {
        try {
            P4Client p4client = PerforceSystem.getP4Client();

            FileStatus status = getUpdatedFileStatus(file);
            if (status == null) {
                return;
            }
            switch (status) {
                case STATUS_VERSIONED_ADD:
                    p4client.actionRevert(file);
                    if (file.exists()) {
                        file.delete();
                    }
                    break;
                case STATUS_VERSIONED_UPTODATE:
                    p4client.actionDelete(file);
                    break;
                case STATUS_VERSIONED_EDIT:
                    p4client.actionRevert(file);
                    if (file.exists()) {
                        p4client.actionDelete(file);
                    }
                    break;
            }
        } catch (PerforceActionException ex) {
            Exceptions.printStackTrace(ex);
        }
    }

    @Override
    public boolean beforeMove(File file, File file1) {
        if (PerforceModuleConfig.getInstance().isPerforceExcluded(file)) {
            return false;
        }
        return true;
    }

    @Override
    @SuppressWarnings("fallthrough")
    public void doMove(File source, File target) throws IOException {
        try {
            P4Client p4client = PerforceSystem.getP4Client();

            FileStatus status = getUpdatedFileStatus(source);
            if (status == null) {
                return;
            }
            switch (status) {
                case STATUS_VERSIONED_UPTODATE:
                    p4client.actionEdit(source);
                case STATUS_VERSIONED_ADD:
                case STATUS_VERSIONED_EDIT:
                    p4client.actionMove(source, target);
            }
        } catch (Exception ex) {
            Exceptions.printStackTrace(ex);
        }
    }

    /**
     * Retrieves the file status. If the cache is not hit, we do it the hard
     * way, we push in cache the value from perforce system.
     *
     * @param file file in question
     * @return the status of the file or null if the file is not handled at all.
     */
    private FileStatus getUpdatedFileStatus(File file) {
        FileStatusCache cache = PerforceSystem.getCache();
        FileInformation fileInfo = cache.getFileInfo(file);
        if (fileInfo == null) {
            cache.refreshFiles(new String[]{file.getAbsolutePath()});
            fileInfo = cache.getFileInfo(file);
        }
        return (fileInfo == null) ? null : fileInfo.getStatus();
    }
 
As I said, foolow the source code and the project PerforceNB on kenai site  at http://kenai.com/projects/perforcenb/.  See you.