Saturday, November 14, 2009

Netbeans Platform Notifications

In any application you need to communicate to the user. Everyone knows how to get information FROM the user, but communicating to the user is something that most people have trouble with.

Sure it's easy to simply display a message box, or flash a label, or give a sound. But doing it right is a completely different topic. People don't think it's important, and therefore there isn't much documentation on the topic.

I'm not going to go too deeply into this, so I'll just give a very broad/simple tip. It doesn't apply to all situations, though in general an error should interrupt the user, and a notification shouldn't. So to achieve this I settled on something where if an error is directly related to an action the user just completed, I would interrupt the user with a modal message box explaining the failed action and when possible the cause. If the action was successful I would also notify the user, but in an unobtrusive manner, though still visible and it should remain available for review.

For this Netbeans has always had a very eye-pleasing solution used by it's automatic update system. These are the balloon messages used to notify you of updates. I did some investigation into doing this and found the NotificationDisplayer class. It displays a balloon message, which has an action associated with it. This message is displayed in the bottom right and linked to a category (defined by it's type and icon). The balloon will hide itself after a few seconds, but remain accessible by clicking the notifications icon in the statusbar. This will display all messages which haven't been dismissed or confirmed. A dismissed message will simply disappear, and a confirmed message (clicking the link) will invoke an ActionListener.

In my case I either display a more detailed message when invoking the ActionListener, or open another view for the action, depending on the origin/cause of the message.

Then, for message boxes I used the DialogDisplayer class.

To access these to notification classes takes quite a one-liner, so I wrapped it into 2 utility classes MessageUtil and NotifyUtil. For your pleasure I listed both of them at the end. You can use them as in the following examples:
MessageUtil.error("Some error message");
MessageUtil.error("Some exception explanation", exceptionInstance);
MessageUtil.info("Some info message");
NotifyUtil.info("Title of Message", "Body of Message");
NotifyUtil.show("Title", "Body", MessageType.INFO, new ActionListener() {
public void actionPerformed(ActionEvent evt)
{
// take some action
}
});

So, here are the classes:
Firstly, the MessageType enumeration in MessageType.java:
package com.blogspot.qbeukes.messages;

import java.net.URL;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import com.blogspot.qbeukes.Log;
import org.openide.NotifyDescriptor;

/**
* Remember to modify this class to locate your icon resources.
* @author qbeukes.blogspot.com
* @license Apache License 2.0
*/
public enum MessageType
{
PLAIN (NotifyDescriptor.PLAIN_MESSAGE, null),
INFO (NotifyDescriptor.INFORMATION_MESSAGE, "info.png"),
QUESTION(NotifyDescriptor.QUESTION_MESSAGE, "question.png"),
ERROR (NotifyDescriptor.ERROR_MESSAGE, "error.png"),
WARNING (NotifyDescriptor.WARNING_MESSAGE, "warning.png");

private int notifyDescriptorType;

private Icon icon;

private MessageType(int notifyDescriptorType, String resourceName)
{
this.notifyDescriptorType = notifyDescriptorType;
if (resourceName == null)
{
icon = new ImageIcon();
}
else
{
icon = loadIcon(resourceName);
}
}

private static Icon loadIcon(String resourceName)
{
URL resource = MessageType.class.getResource("icons/" + resourceName);
if (resource == null)
{
Log log = Log.getLog(MessageType.class);
log.warn("Failed to load NotifyUtil icon resource: " + resourceName + ". Using blank image.");
return new ImageIcon();
}
return new ImageIcon(resource);
}

int getNotifyDescriptorType()
{
return notifyDescriptorType;
}

Icon getIcon()
{
return icon;
}
}

Then the MessageUtil for display message boxes, MessageUtil.java
package com.blogspot.qbeukes.messages;

import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;

/**
* For displaying message boxes
* @author qbeukes.blogspot.com
* @license Apache License 2.0
*/
public class MessageUtil
{
private MessageUtil() {}

/**
* @return The dialog displayer used to show message boxes
*/
public static DialogDisplayer getDialogDisplayer()
{
return DialogDisplayer.getDefault();
}

/**
* Show a message of the specified type
*
* @param message
* @param messageType As in {@link NotifyDescription} message type constants.
*/
public static void show(String message, MessageType messageType)
{
getDialogDisplayer().notify(new NotifyDescriptor.Message(message,
messageType.getNotifyDescriptorType()));
}

/**
* Show an exception message dialog
*
* @param message
* @param exception
*/
public static void showException(String message, Throwable exception)
{
getDialogDisplayer().notify(new NotifyDescriptor.Exception(exception, message));
}

/**
* Show an information dialog
* @param message
*/
public static void info(String message)
{
show(message, MessageType.INFO);
}

/**
* Show an error dialog
* @param message
*/
public static void error(String message)
{
show(message, MessageType.ERROR);
}

/**
* Show an error dialog for an exception
* @param message
* @param exception
*/
public static void error(String message, Throwable exception)
{
showException(message, exception);
}

/**
* Show an question dialog
* @param message
*/
public static void question(String message)
{
show(message, MessageType.QUESTION);
}

/**
* Show an warning dialog
* @param message
*/
public static void warn(String message)
{
show(message, MessageType.WARNING);
}

/**
* Show an plain dialog
* @param message
*/
public static void plain(String message)
{
show(message, MessageType.PLAIN);
}
}

And for displaying balloon messages, NotifyUtil.java:
package com.blogspot.qbeukes.messages;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import org.openide.awt.NotificationDisplayer;

/**
* For notifying the user.
* @author qbeukes.blogspot.com
* @license Apache License 2.0
*/
public class NotifyUtil
{
private NotifyUtil() {}

/**
* Show message with the specified type and action listener
*/
public static void show(String title, String message, MessageType type, ActionListener actionListener)
{
NotificationDisplayer.getDefault().notify(title, type.getIcon(), message, actionListener);
}

/**
* Show message with the specified type and a default action which displays the
* message using {@link MessageUtil} with the same message type
*/
public static void show(String title, final String message, final MessageType type)
{
ActionListener actionListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e)
{
MessageUtil.show(message, type);
}
};

show(title, message, type, actionListener);
}

/**
* Show an information notification
* @param message
*/
public static void info(String title, String message)
{
show(title, message, MessageType.INFO);
}

/**
* Show an error notification
* @param message
*/
public static void error(String title, String message)
{
show(title, message, MessageType.ERROR);
}

/**
* Show an error notification for an exception
* @param message
* @param exception
*/
public static void error(String title, final String message, final Throwable exception)
{
ActionListener actionListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e)
{
MessageUtil.showException(message, exception);
}
};

show(title, message, MessageType.ERROR, actionListener);
}

/**
* Show an warning notification
* @param message
*/
public static void warn(String title, String message)
{
show(title, message, MessageType.WARNING);
}

/**
* Show an plain notification
* @param message
*/
public static void plain(String title, String message)
{
show(title, message, MessageType.PLAIN);
}
}

I originally developed this code under Apache License 2.0. So you can use it under the same license.

1 comment:

rehevkor5 said...

Very nice, but you should probably replace your icon code with ImageUtilities.loadImageIcon().