Chapter 17. Tips and Tricks: Let's Get Some Cool Expert Tips!

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.

Asynchronous Initialization of Graphic Components

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.

Undo/Redo

TopComponents 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.

Ending an Application's Lifecycle

When a NetBeans Platform application is shut down, all user-specific settings (such as the information about open TopComponents, 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

WarmUp Tasks

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).

System Tray

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);
      }
   }
}

Desktop

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

isDesktopSupported()

Checks for Desktop class support on the operating system.

isSupported(Desktop.Action a)

Checks if actions like BROWSE, OPEN, EDIT, PRINT, and MAIL are available.

getDesktop()

Used to get an instance of the Desktop class. This method throws an UnsupportedOperationException if the Desktop class is not supported.

browse(URI uri)

Opens the given URI in the file browser.

open(File file)

Opens the file in the associated program (or in a file browser, if it is a folder).

edit(File file)

Opens the file in the standard editor for this file type.

print(File file)

Sends the file directly to the printer using the standard file application print functionality.

mail()

Opens the e-mail edit window.

mail(URI uri)

Opens the e-mail edit window where the mailto field is filled with the e-mail address from URI.

Logging

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.

Logger

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.

LogManager

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);
}

Configuration

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.

Error Reports

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);
}

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.163.28