This chapter looks at several helpful interfaces, classes, and concepts of the NetBeans Platform. Some of the JDK's newest features are also demonstrated in action on the NetBeans Platform, such as the SwingWorker
class.
When developing GUIs, it is important to maintain fast response time throughout the component lifecycle. This is especially true for the phase when components are initialized. A good example of this is with wizards, as discussed in Chapter 8. If the user starts a wizard, the wizard should open and be available immediately. Sometimes data for components needs to load from a relatively slow data source or must be calculated from dependent data. In this case, initialize your components in a separate thread asynchronously for all initializations in the user interface. When doing so, take care to not access GUI components from outside the event dispatch thread (EDT).
The NetBeans Utilities API provides an easy way to meet this requirement: the service provider interface AsyncGUIJob
. This interface specifies two methods to help initialize components asynchronously. The construct()
method is executed automatically in a separate thread, so the EDT is not blocked. This lets you load data or perform other long-running initializations without performance being affected. Do not access GUI components in the construct()
method, however. Rather, as soon as the construct()
method has returned, the finished()
method is called, within the EDT. Here, you can add data previously loaded in the construct()
method.
In the example in Listing 17-1, data is added (loaded in construct()
) to a DefaultComboBoxModel
. After loading, you add the created data model to the JComboBox
within the finished()
method. This asynchronous process is started and connected to the component using the method Utilities.attachInitJob()
. This way, a number of components are defined and started independently.
Example 17.1. Asynchronously initializing graphical components using the AsyncGUIJob interface
public final class AsynchTopComponent extends TopComponent { private JComboBox items = new JComboBox(new String[] { "Loading..." }); private DefaultComboBoxModel m = new DefaultComboBoxModel(); private AsynchTopComponent() { initComponents(); Utilities.attachInitJob(items, new AsyncGUIJob(){ public void construct() { // long-lasting loading of data for(int i = 0; i < 20; i++) { Thread.sleep(200); m.addElement(new String("Item " + i)); } } public void finished() { items.setModel(m); } }); } }
Another possibility for asynchronously initializing GUI components is the SwingWorker
class, which became part of the standard Java API in version 6. It is an abstract class, initializing components in almost the same way as via the AsyncGUIJob
interface. Using the SwingWorker
class, the previous example with AsyncGUIJob
looks like Listing 17-2.
Example 17.2. Asynchronously initializing graphic components using the SwingWorker class
SwingWorker<DefaultComboBoxModel, String> worker = new SwingWorker<DefaultComboBoxModel, String>() { protected DefaultComboBoxModel doInBackground() throws Exception { // long-lasting loading of data for(int i = 0; i < 20; i++) { Thread.sleep(200); m.addElement(new String("Item " + i)); } return m; } protected void done() { try { items.setModel(get()); } catch (Exception ignore) {} } }; worker.execute();
Similar to the construct()
method, data is created (or loaded) within the method doInBackground()
. The difference occurs when passing the created data as a return value of the function (see Listing 17-3). The return type is defined by the first template of the SwingWorker
class—in this example, DefaultComboBoxModel
. This method is also executed outside the EDT. The done()
method is the counterpart to the finished()
method, which is called from within the EDT as soon as the doInBackground()
method has finished. Using the get()
method, we receive data prepared by doInBackground()
.
Other very useful features of the SwingWorker
class are the publish()
and process()
methods. By using publish()
, data can be sent from the asynchronously executed doInBackground()
method to the EDT that is processed by calling process()
.
Example 17.3. Publishing and processing with the SwingWorker class
items.setModel(m); SwingWorker<DefaultComboBoxModel, String> worker = new SwingWorker<DefaultComboBoxModel, String>() { protected DefaultComboBoxModel doInBackground() throws Exception { for(int i = 0; i < 20; i++) { Thread.sleep(200); publish(new String("Item " + i)); } return m; } protected void process(List<String> chunks) { m.addElement(chunks.iterator().next()); } }; worker.execute();
Rather than setting the data model in the done()
method, the elements are added immediately. In the doInBackground()
method, single entries are immediately sent to the EDT using publish()
. Those entries are received with the process()
method and inserted into the combo box, so they appear right away. The parameter types publish()
and process()
are defined in the second template of the SwingWorker
class.
TopComponent
s and multiview elements provide undo/redo-functionality for the user. This functionality is specified by the UndoRedo
interface, implemented by the UndoRedo.Manager
class. It handles the undo and redo actions provided by the NetBeans Platform in the Edit menu and toolbar. This manager derives from the class UndoManager
in the Java API, which administrates changes liable to being undone or restored. An instance of this manager is retrieved by calling the getUndoRedo()
function.
Events added to the manager are strongly dependent on context. The interface for those events is specified by UndoableEdit
. Java already provides abstract classes for this interface. The class AbstractUndoableEdit
, for example, provides a standard implementation for all methods, limiting override to only the classes needing special implementation. The StateEdit
class and the related StateEditable
class are very handy in this context as well.
The StateEditable
interface is implemented by objects whose data may be changed by users. An example of this would be a DataObject
class representing an MP3 file whose ID3 information requires a change by the user.
The example in Listing 17-4 demonstrates this principle with a very simple class that has one attribute in a text field that is editable by the user. The UndoRedo.Manager
is held as a private data element, retrieved by calling the getUndoRedo()
method. The TopComponent
has two buttons: one to read the attribute from the data object, the other to save the data from the text field.
If a change is performed, a StateEdit
object is created that implements the UndoableEdit
interface. This object needs an instance of the StateEditable
interface. This, in turn, is the data object. The UndoableEdit
instance is passed to the manager by calling the undoableEditHappened()
method that updates all listeners. That way, all platform Undo and Redo buttons are activated or deactivated automatically. Now all changes can be applied to the data object, and the event is ended by the end()
method.
Example 17.4. Providing an undo/redo manager and adding an element when data is changed by the user
public class MyTopComponent extends TopComponent { private UndoRedo.Manager manager = new UndoRedo.Manager(); private MyObject obj = new MyObject(); public UndoRedo getUndoRedo() { return manager; } private void loadActionPerformed(ActionEvent evt) { textField.setText(obj.getProp()); } private void saveActionPerformed(ActionEvent evt) { StateEdit edit = new StateEdit(obj); manager.undoableEditHappened(new UndoableEditEvent(obj, edit)); obj.setProp(textField.getText()); edit.end(); } }
The data object needing changes undone or restored implements the StateEditable
interface. This interface specifies the methods storeState()
and restoreState()
. The principle of the StateEdit
class is based on storing data object attributes to a Hashtable
object. This hashtable is managed by the StateEdit
object. The storeState()
method is called when the StateEdit
object is created. Attributes are stored to the hashtable (that is, passed) before changes are applied.
To undo changes, the StateEdit
object calls the restoreState()
method, and a hashtable that holds the original values is passed as a parameter. Those values need only be read and applied as shown in Listing 17-5.
Example 17.5. Data object whose changed attributes need to be restored
public class MyObject implements StateEditable { private String prop = new String("init value"); public void storeState(Hashtable<Object, Object> props) { props.put("prop", prop); // save original state } public void restoreState(Hashtable<?, ?> props) {
prop = (String)props.get("prop"); // read original state } public void setProp(String value) { prop = value; } public String getProp() { return prop; } }
Finally, we'll demonstrate how easy it is to add undo/redo functionality to a text component. Text components especially need this feature often. All subclasses of JTextComponent
( e.g. JEditorPane, JTextArea
und JTextField
) use a Document
as a data model.
An UndoableEditListener
can be added to a Document
instance by calling the addUndoableEditListener()
method. This listener interface is implemented by the NetBeans UndoRedo. Manager
. This manager, previously stored in the TopComponent
and returned by the getUndoRedo()
method, is added to a Document
instance as a listener. By appending a single line of code, you can add undo/redo functionality to a text component:
textField.getDocument().addUndoableEditListener(manager);
Now the text component is able to report its events to the manager, automatically activating or deactivating the Undo and Redo buttons. You can add undo support to a text component, as well as any component whose data model implements the Document
interface or uses an implementation of Document
, as the HTMLDocument
and PlainDocument
classes do.
When a NetBeans Platform application is shut down, all user-specific settings (such as the information about open TopComponent
s, application window size, and toolbars) are saved to the application user directory. In addition, all modules that implement a module installer (see Chapter 3) are asked if the application can be shut down. Thus, an application is not only closed, but it is shut down properly. Usually, an application is closed using the menu or the Close button in the title bar. In some cases, you might close an application programmatically. This could be an option if wrong data is entered in a login dialog, and the application should then be closed. In this case, you must not or cannot—as usual in Java applications—close the application using System.exit()
. The process for shutting down an application is specified by the Utilities API in the global service LifecycleManager
. The NetBeans Core
module offers a service provider for that purpose, responsible for executing the tasks mentioned earlier. This standard implementation of the LifecycleManager
can be obtained by calling the getDefault()
method. Close an application by calling the following line:
LifecycleManager.getDefault().exit();
Since this LifecycleManager
is implemented as a service, you can provide your own implementation of this abstract class. This does not mean that the standard implementation of the NetBeans Platform is no longer available—you simply need to call it. This way, it is possible to execute custom tasks before an application is closed. Listing 17-6 demonstrates how to call the standard implementation after executing custom tasks, and shut down applications properly.
Example 17.6. A custom LifecycleManager implementation, which calls the standard implementation
public class MyLifecycleManager extends LifecycleManager { public void saveAll() { for(LifecycleManager manager : Lookup.getDefault().lookupAll(LifecycleManager.class)) { if(manager != this) { /* only call the Core Manager */ manager.saveAll(); } } } public void exit() { // perform application-specific shutdown tasks for(LifecycleManager manager : Lookup.getDefault().lookupAll(LifecycleManager.class)) { if(manager != this) { /* only call the Core Manager */ manager.exit(); } } } }
This implementation must be registered as a service provider. It's important to note that a position must be declared to ensure that the custom implementation is delivered by the Lookup and called first. The LifecycleManager
would be called only if this is not done. The class is registered in the META-INF/services
folder (see Chapter 6), in the file org.openide.LifecycleManager
, which contains the following two lines:
com.galileo.netbeans.module.MyLifecycleManager #position=1
The NetBeans Platform offers an extension point named WarmUp
, for executing asynchronous tasks when starting applications:
<folder name="WarmUp"> <file name="com-galileo-netbeans-module-MyWarmUpTask.instance"/> </folder>
You can add any number of instances (that implement the Runnable
interface) to this extension point in the layer file:
public class MyWarmUpTask implements Runnable { public void run() { // do something on application startup } }
Critical tasks—for example, tasks that are necessary as module-starting conditions—must not be started here. These tasks are executed asynchronously at the start of applications, which means there is no guarantee about when the task is started or finished. In this case, a module installer should be used (see Chapter 3).
Java includes enhanced desktop integration in version 6 and provides access to the system tray of the underlying operating system. You can add one or more icons with a context menu or double-click action. A good way to do this for a NetBeans application is with the restored()
method of the module installer (see Chapter 3). First, check whether the operating system has a system tray. If so, gain access using the getSystemTray()
method. In order to add a context menu, create a PopupMenu
whose actions are defined via an extension point in the layer file. Thus, actions are added to the tray icon from different modules. The extension point used is called TrayMenu
, whose values are read using a Lookup. The registered actions only need to implement the Action
interface, which means NetBeans Platform action classes may also be used. After creating the context menu, pass the menu, an icon, and a tooltip to a TrayIcon
object and add it to the system tray, as shown in Listing 17-7.
Example 17.7. Adding a system tray icon whose context menu is built using a layer file
public void restored() { if (SystemTray.isSupported()) { SystemTray tray = SystemTray.getSystemTray(); PopupMenu popup = new PopupMenu(); popup.setFont(new Font("Arial", Font.PLAIN, 11)); for(Action a : Lookups.forPath("TrayMenu").lookupAll(Action.class)) { MenuItem item = new MenuItem((String)a.getValue(Action.NAME)); item.addActionListener(a); popup.add(item); } Image img = ImageUtilities.loadImage("com/galileo/netbeans/module/icon.gif"); TrayIcon trayIcon = new TrayIcon(img, "My Tray Menu", popup); trayIcon.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { System.out.println("double click on tray icon"); } }); try { tray.add(trayIcon); } catch (AWTException e) { System.err.println(e); } } }
The Desktop
class in Java 6 allows execution of standard applications like an Internet browser or an e-mail client. Pass a File
or URI
object to the methods provided by the Desktop
class. On the basis of these objects, an associated standard application is launched. For example, if Desktop.open(new File("myfile.pdf"))
is executed, Acrobat Reader is started (if this is the standard application for .pdf
files). Table 17-1 shows all methods of the Desktop
class.
Table 17.1. Methods in the Desktop class
Method | Function |
---|---|
| Checks for |
| Checks if actions like |
| Used to get an instance of the |
| Opens the given URI in the file browser. |
| Opens the file in the associated program (or in a file browser, if it is a folder). |
| Opens the file in the standard editor for this file type. |
| Sends the file directly to the printer using the standard file application print functionality. |
| Opens the e-mail edit window. |
| Opens the e-mail edit window where the mailto field is filled with the e-mail address from URI. |
A very important and helpful (but often disregarded) topic is logging. Logging is the practice of recording status, warning, and error messages. Logging in the NetBeans Platform is based on the Java Logging API.
Log output is recorded by the Logging API using a Logger
object. Typically, different components have different Logger
instances. You get an instance of a Logger
via the factory method getLogger()
. You can also use a global logger, but you should use a named, component-specific logger whenever possible. This way, different loggers can be turned on or off, which is very helpful when searching for bugs. A named logger is obtained by the following:
Logger log = Logger.getLogger(MyClass.class.getName());
Typically, the full name of the class that creates the log output is used as the name for the logger. This name is obtained from the getName()
method. If a logger already exists for this name, it is returned. The global logger can be obtained using the name Logger.GLOBAL_LOGGER_NAME
.
Record log output (of a defined level) using the log()
methods in the Logger
class. The following log levels are provided in the Level
class: FINEST, FINER, FINE, CONFIG, INFO, WARNING
, and SEVERE
. The methods finest(), finer(), fine(), config(), info(), warning()
, and severe()
are also provided; these record the given message at the declared level.
The Java Logging API specifies a central LogManager
. This manager controls a hierarchical namespace holding all named loggers. That's why it's reasonable to use the full names of classes (that hold the hierarchical package structure) for logger names. For access to this manager, use the following:
LogManager manager = LogManager.getDefault();
The LogManager
provides all names of all loggers as well as the name of a NetBeans Platform logger whose level may be changed for debugging purposes. A list of all loggers can be retrieved as follows:
LogManager manager = LogManager.getLogManager(); for(String name : Collections.list(manager.getLoggerNames())) { System.out.println(name); }
The manager also administers configuration files, which are initially loaded from the lib/logging.properties
file in the JRE folder. Define configuration files by setting them to the system property java.util.logging.config.file
. Configuration data may be loaded from a database. Implement a class that reads the data from a database and register it to the system property java.util.logging.config.class
. Registration causes it to be automatically instantiated. Within this class, provide the configuration data for the LogManager
via an InputStream
for the readConfiguration(InputStream)
method in the LogManager
.
Register Handler
implementations in the configuration file so they output log data to the console (ConsoleHandler
) or into a file (FileHandler
). You can register implementations like the handler from the NetBeans Platform that displays log messages graphically. The logging system comes with a root logger. All other loggers forward their logs to this root logger. For this root logger, register a handler with the following property:
handlers = java.util.logging.ConsoleHandler
Multiple handlers can be listed using commas. To disable forwarding logs to the root logger, do so by using the following:
<logger name>.useParentHandlers = false
Define a handler especially for this logger in order to obtain log output:
<logger name>.handlers = java.util.logging.FileHandler
Setting the log level is important in the configuration. A log level defines which kind of logs are recorded. For example, set the log level globally as follows to hide simple status messages but show warning or error messages when debugging:
.level = WARNING
Or overwrite a single logger's log level by using its name as a prefix:
<logger name>.level = INFO
Configuration data is not only set in the configuration file, but also as system properties. Set it at runtime using the System.setProperty()
method. It is important to call the LogManager
's readConfiguration()
method in order to apply the new configuration data. Alternatively, select the configuration at the application's startup using command-line parameters. During development in NetBeans, set your NetBeans Platform application start parameters in the project properties file (under Important Files
) using the property run.args.extra
:
run.args.extra = -J-Dcom.galileo.netbeans.myclass.level=INFO
For distribution of your application, set command-line parameters using the property default_options
in the etc/<application>.conf
file.
The NetBeans Platform implements and registers a special log handler that displays recorded error messages for the user in a dialog. Therefore, use either the SEVERE
or WARNING
log level, and pass the Exception
directly to the log()
method.
Logger logger = Logger.getLogger(MyClass.class.getName()); try { ... } catch(Exception e) { logger.log(Level.SEVERE, null, e); // or logger.log(Level.WARNING, null, e); }
This chapter provided a range of useful tips. We first looked at an approach for initializing GUI components asynchronously on the EDT. In this context, you also learned how to use the SwingWorker
class, which is part of JDK 6.
Another tip covered undo/redo functionality in several components in a NetBeans Platform application. You also saw how to execute tasks when the application shuts down, as well as how to execute long-running tasks asynchronously during the application's startup process.
In the final sections, we looked at the new JDK 6 SystemTray
and Desktop
classes, as well as the NetBeans Platform logging facilities.
3.138.106.233