Chapter 24. Executing Scores

In Chapter 21, we explored how to support execution of scores in a simple fashion: by directly interpreting the textual stream of the score using the Score class and converting it to a JavaSound audio stream to be played immediately.

However, since then we have added the ability to compile scores into standard audio files. These could be played instead of the score file itself. One way of playing these audio files is similar to what we did before: the audio file can be opened as a byte stream by the IDE when Execute is selected by the user, and this stream fed to a sound channel to be played.

There is another possibility. Most operating systems that support sound output include some kind of player that can play sound files when run as a command. The common AU format that our compiler produces is generally understood by such players. For example, on Windows 2000, the mplayer2.exe command can be passed an audio file; on many Linux systems, play does the same.

Which kind of player a user should use—internal, using JavaSound, or external, using an operating-system-dependent executable—is the user’s choice. Some platforms may have good JavaSound support but no installed executable player. Others may have a great player with looping and pause support and a graphic equalizer, and JavaSound may not provide the best option (even if the score support module were to implement such features). So this chapter will demonstrate how to let the user decide, in general as well as for specific scores, exactly how to play sound files.

The NetBeans APIs have a general system whereby modules may define service types, which are Java classes that implement some general interface providing a kind of service to the system and that may be installed, configured, ordered, selected, and persisted. In the case of execution, the standard service type interface is Executor, an abstract class in the Execution API. We will define two such executors. The actual ExecCookie will not play the audio file, but will instead find the executor selected by the user and delegate work to it.

Creating the .au Player Executor

Here we will define two subclasses of Executor in turn. The implementations will look quite different, as the NetBeans Execution API has direct support for making external execution convenient, while internal execution is more free-form.

The Internal Player

To make an internal audio file player, we will directly extend Executor, as shown in Example 24-1.

Example 24-1. Minicomposer: src/org/netbeans/examples/modules/minicomposer/InternalPlayer.java

public class InternalPlayer extends Executor {
  public ExecutorTask execute(DataObject obj) throws IOException {
    if (!(obj instanceof ScoreDataObject)) {
      IOException ioe = new IOException("Wrong type: " + obj);
      TopManager.getDefault( ).getErrorManager( ).annotate(ioe,
        NbBundle.getMessage(InternalPlayer.class, "EXC_wrong_type",
                            obj.getLoader( ).getDisplayName( )));
      throw ioe;
    }
    FileObject fo = ScoreDataLoader.findAudioFile((ScoreDataObject)obj);
    if (fo == null) // throw another IOException with localized message...
    final File f = FileUtil.toFile(fo);
    if (f == null) // throw yet another IOException...
    class Job implements Runnable {
      public void run( ) {
        try {
          AudioInputStream ais = AudioSystem.getAudioInputStream(f);
          AudioFormat format = ais.getFormat( );
          DataLine.Info dlinfo = new DataLine.Info(Clip.class, ais.getFormat( ));
          Clip clip = (Clip)AudioSystem.getLine(dlinfo);
          clip.open(ais);
          clip.start( );
          try {
            while (clip.isActive( )) {
              Thread.sleep(1000);
            }
          } catch (InterruptedException ie) {
            // ignore - job stopped
          }
          clip.stop( );
          clip.close( );
        } catch (Exception e) {
          // Will appear in Output Window.
          e.printStackTrace( );
        }
      }
    }
    return TopManager.getDefault( ).getExecutionEngine( ).execute(
      NbBundle.getMessage(ScoreExecSupport.class, "LBL_audio_play_process"),
      new Job( ), null);
  }
  private static final long serialVersionUID = -3129235161777547136L;
  public ExecutorTask execute(ExecInfo info) throws IOException {
    throw new IllegalStateException("not called");
  }
  public HelpCtx getHelpCtx( ) {
    return new HelpCtx("org.netbeans.examples.modules.minicomposer");
  }
}

The worker method is execute(DataObject). This is given an object to execute in some fashion and is expected to provide a task it has started, which permits clients to listen to completion of the task asynchronously and query its status.

The first steps involve sanity-checking of the data object argument. If something other than a score object was passed in, this is a user error; the user may have inadvertently associated the internal player with a Java source file, for example. So an IOException is thrown. It is made more pleasant, however, with a localized annotation. This is a bit of friendly and localizable text that gives the exception meaning to the user, ideally explaining how the problem could be corrected. In this case the bundle file reads as follows:

# {0} - display name of type of strange object
EXC_wrong_type=Cannot "play" an object of type {0} as a sound file

The comment is a hint for future translators when constructing alternate wordings. When ErrorManager (which is discussed in more detail in Chapter 27) is asked to add a localized annotation to an exception, it remembers that phrase; later when the exception is actually notified again using ErrorManager (in this case by ExecSupport, which is calling the executor), a polite dialog box is displayed to the user giving the localized message and prompting him to select an alternate executor.

In the same fashion, the executor looks for the audio file within the score support. This is handled by a utility method we will add to the ScoreDataLoader class:

  public static FileObject findAudioFile(ScoreDataObject obj) {
    Set seconds = obj.secondaryEntries( ); // Set<MultiDataObject.Entry>
    if (seconds.isEmpty( )) return null;
    MultiDataObject.Entry entry =
      (MultiDataObject.Entry)seconds.iterator( ).next( );
    return entry.getFile( );
  }

If the audio file is not found, another exception is thrown with a different localized message (not shown for brevity). This might happen if the user turned off Run Compilation in Execution Settings and the score file was not compiled at all.

We also try to find the disk file represented by the score. Recall that a FileObject might be an entry in a JAR, or even something more exotic like a file on a remote FTP server. We can get input streams from any file object, but some uses of JavaSound require a stream from a file on disk, with a known length and seekable. So we explicitly prevent non-local audio files from being played. FileUtil.toFile( ) translates a file object to a disk file if it can, otherwise returning null—in which case we throw our third localized exception.

The Job local class actually plays the audio file now given by f. The details are vanilla JavaSound. To run the job and produce a task representing its status and completion, we use the system’s execution engine as specified in the Execution API. This is a singleton service provided by the IDE that manages concurrent execution of processes. The NetBeans standard implementation can run jobs in private thread groups for maximum isolation, optionally displays a list of running processes in the Execution window, and supports terminating runaway processes by the user (in this same window).

We give the execution engine three parameters when starting the job. First, a display name for the process that will be run; since it is not null, the process will be displayed in the Execution window. Second, the runnable job itself. The third parameter is optional and is an InputOutput, or a handle to a tab in the Output Window; by default a fresh tab is created, labeled according to the supplied display name.

You can suppress any output if you want; here we permit the tab to appear. If some problem arises while playing the file and an exception is thrown—this can happen if JavaSound is incorrectly configured and cannot find a sound driver, for example—it is caught in Job.run( ) and printed to standard error. The execution engine automatically reroutes uses of standard I/O streams occurring within jobs it is executing, binding them to the Output Window. Otherwise, such messages would appear on the IDE’s own console (or log file). It also traps calls to System.exit( ) and terminates the executed process rather than the whole IDE, though that does not concern us here.

Finally some miscellanea: The line defining serialVersionUID indicates our willingness to retain compatibility of serialized InternalPlayers across releases of the module. execute(ExecInfo) is an older method in Execute more suited to executing Java classes than arbitrary objects. So long as the newer execute(DataObject) is overridden, it will not be called by the system. getHelpCtx( ) is used to associate JavaHelp with the executor; help contexts will be discussed later in Chapter 27.

In addition to the player, it is usual to provide a BeanInfo. In general, services can be customized by the user—this one cannot, but it will be displayed alongside others that can. The BeanInfo can control the display name, tool tip, icon, help associations of the service, and more importantly properties and graphical customizers. This class is shown in Example 24-2.

Example 24-2. Minicomposer: src/org/netbeans/examples/modules/minicomposer/InternalPlayerBeanInfo.java

public class InternalPlayerBeanInfo extends SimpleBeanInfo {
  public BeanInfo[ ] getAdditionalBeanInfo( ) {
    try {
      return new BeanInfo[ ] {Introspector.getBeanInfo(Executor.class)};
    } catch (IntrospectionException ie) {
      TopManager.getDefault( ).getErrorManager( ).notify(ie);
      return null;
    }
  }
  public BeanDescriptor getBeanDescriptor( ) {
    BeanDescriptor desc = new BeanDescriptor(InternalPlayer.class);
    desc.setDisplayName(NbBundle.getMessage(InternalPlayerBeanInfo.class,
                                            "LBL_InternalPlayer"));
    desc.setShortDescription(NbBundle.getMessage(InternalPlayerBeanInfo.class,
                                                 "HINT_InternalPlayer"));
    return desc;
  }
  public Image getIcon(int type) {
    if (type == BeanInfo.ICON_COLOR_16x16 || type == BeanInfo.ICON_MONO_16x16) {
      return Utilities.loadImage(
        "org/netbeans/examples/modules/minicomposer/InternalPlayerIcon.gif");
    } else {
      return null;
    }
  }
}

We first explicitly inherit any info provided by the superclass, Executor; Executor has no interesting properties so this means just the Identifying Name property permitting the user to rename services. The bean descriptor can set a display name and tool tip for the service—though as we will see shortly, the XML layer provides a quicker way of specifying this information, which does not require the InternalPlayerBeanInfo to be loaded in many cases. getIcon( ) provides a 16 X 16 icon for the service:

image with no caption

Recall that Utilities.loadImage( ) loads images from module JARs and does caching.

The External Player

The external player will be implemented differently. Extending the convenience base class ProcessExecutor (as shown in Example 24-3), the player will not directly handle the running of the process. Direct manipulation of external processes can become rather complicated: creating a customizable command line, launching it using Java’s execution APIs, proxying I/O streams, and controlling termination. By extending ProcessExecutor, we can instead just supply a template for the command line to run, and the rest is automatic. The user can then edit this command line if necessary.

Example 24-3. Minicomposer: src/org/netbeans/examples/modules/minicomposer/ExternalPlayer.java

public class ExternalPlayer extends ProcessExecutor {
  private static final NbProcessDescriptor DEFAULT = new NbProcessDescriptor(
    Utilities.isWindows( ) ?
      ""C:\Program Files\Windows Media Player\mplayer2.exe"" :
      "play",
    (Utilities.isWindows( ) ? "/Play " : "") +
      "{" + MyFormat.TAG_AUFILE + "}",
    NbBundle.getMessage(ExternalPlayer.class, "MSG_format_hint")
  );
  public ExternalPlayer( ) {
    setExternalExecutor(DEFAULT);
  }
  protected Process createProcess(DataObject obj) throws IOException {
    if (!(obj instanceof ScoreDataObject)) {
      // throw localized IOException as before...
    }
    FileObject fo = ScoreDataLoader.findAudioFile((ScoreDataObject)obj);
    if (fo == null) // again as before...
    final File f = FileUtil.toFile(fo);
    if (f == null) // once again as before...
    return getExternalExecutor( ).exec(new MyFormat(f));
  }
  private static final long serialVersionUID = -4397529002559509129L;
  protected Process createProcess(ExecInfo info) throws IOException {
    throw new IllegalStateException("Should not be called");
  }
  public HelpCtx getHelpCtx( ) {
    return new HelpCtx("org.netbeans.examples.modules.minicomposer");
  }
  private static class MyFormat extends MapFormat {
    static final String TAG_AUFILE = "aufile";
    private static final long serialVersionUID = 6980703950237286310L;
    MyFormat(File aufile) {
      super(new HashMap(1));
      getMap( ).put(TAG_AUFILE, aufile.getAbsolutePath( ));
    }
  }
}

Our first job is to specify the default command line that will be run to play the audio file. NbProcessDescriptor is a data structure holding the name of an executable program (file), as well as a string containing its arguments, and a legend for substitutions that can be made. The constructor sets this process descriptor as the default initial value for the executor.

The first parameter we pass when creating the DEFAULT descriptor is the executable. Here we test for the Windows operating system, and if so, use mplayer2.exe; otherwise, play is used, a common command on Linux. Of course this list of defaults could be expanded to cover more operating systems, though the user can always correct a bad guess.

The next parameter is the list of arguments. Actually, they are all given as a single string, as if ready to pass into a command shell such as the Unix Bourne shell (NbProcessDescriptor is responsible for handling platform-specific quoting conventions). mplayer2.exe requires a /Play switch; otherwise, we can just pass the file name. Rather than hard-code the audio filename here, we use the key {aufile} that will be substituted at runtime (more on this in a moment).

The legend for the process descriptor is displayed to the user when editing command lines based on it in a customizer dialog box. Normally this provides an explanation of any keys that may be used in the command line. Thus our bundle file contains

MSG_format_hint={aufile} = full path to AU file to play

createProcess(DataObject) is called when executing the object, to provide a Java handle for an external process. As with the internal player, createProcess(ExecInfo) is only used if you do not override this method. Again we start by doing some sanity checking on the input and throw localized IOExceptions if there is anything wrong, to help the user correct the problem. The actual creation of the external process is done by the exec( ) method on the configured process descriptor.

exec( ) normally takes a format for substituting keys in the process descriptor. It can also take a working directory and several other parameters covered in the Javadoc. This format is a general Java text format; it is applied to both the process name and arguments and is expected to result in the final command line. When applied to NbProcessDescriptors, the normal format to use is a MapFormat (a utility class in org.openide.util). MapFormat permits flexible substitution of named keys via a translation map. Our subclass MyFormat defines just one key, TAG_AUFILE, expected to map to the absolute path of the audio file. When such a format is made, the file path is specified by the executor and added to its (otherwise empty) map.

Again we provide a BeanInfo describing the executor; see Example 24-4.

Example 24-4. Minicomposer: src/org/netbeans/examples/modules/minicomposer/ExternalPlayerBeanInfo.java

public class ExternalPlayerBeanInfo extends SimpleBeanInfo {
  public BeanInfo[ ] getAdditionalBeanInfo( ) {
    try {
      return new BeanInfo[ ] {Introspector.getBeanInfo(ProcessExecutor.class)};
    } catch (IntrospectionException ie) {
      TopManager.getDefault( ).getErrorManager( ).notify(ie);
      return null;
    }
  }
  public BeanDescriptor getBeanDescriptor( ) {
    BeanDescriptor desc = new BeanDescriptor(ExternalPlayer.class);
    desc.setDisplayName(NbBundle.getMessage(ExternalPlayerBeanInfo.class,
                                            "LBL_ExternalPlayer"));
    desc.setShortDescription(NbBundle.getMessage(ExternalPlayerBeanInfo.class,
                                                 "HINT_ExternalPlayer"));
    return desc;
  }
  public PropertyDescriptor[ ] getPropertyDescriptors( ) {
    try {
      PropertyDescriptor classPath =
        new PropertyDescriptor("classPath", ProcessExecutor.class);
      classPath.setHidden(true);
      PropertyDescriptor bootClassPath =
        new PropertyDescriptor("bootClassPath", ProcessExecutor.class);
      bootClassPath.setHidden(true);
      PropertyDescriptor repositoryPath =
        new PropertyDescriptor("repositoryPath", ProcessExecutor.class,
                               "getRepositoryPath", null);
      repositoryPath.setHidden(true);
      PropertyDescriptor libraryPath =
        new PropertyDescriptor("libraryPath", ProcessExecutor.class,
                               "getLibraryPath", null);
      libraryPath.setHidden(true);
      PropertyDescriptor environmentVariables =
        new PropertyDescriptor("environmentVariables", ProcessExecutor.class);
      environmentVariables.setHidden(true);
      PropertyDescriptor workingDirectory =
        new PropertyDescriptor("workingDirectory", ProcessExecutor.class);
      workingDirectory.setHidden(true);
      PropertyDescriptor appendEnvironmentVariables =
        new PropertyDescriptor("appendEnvironmentVariables",
                               ProcessExecutor.class);
      appendEnvironmentVariables.setHidden(true);
      return new PropertyDescriptor[ ] {
        classPath, bootClassPath, repositoryPath, libraryPath,
        environmentVariables, workingDirectory, appendEnvironmentVariables
      };
    } catch (IntrospectionException ie) {
      TopManager.getDefault( ).getErrorManager( ).notify(ie);
      return null;
    }
  }
  public Image getIcon(int type) {
    if (type == BeanInfo.ICON_COLOR_16x16 || type == BeanInfo.ICON_MONO_16x16) {
      return Utilities.loadImage(
        "org/netbeans/examples/modules/minicomposer/ExternalPlayerIcon.gif");
    } else {
      return null;
    }
  }
}

The additional bean info, bean descriptor, and icon are similar to the internal player’s bean info. For the property descriptors, we need do a little more work—the ProcessExecutor superclass, which was originally designed with a Java IDE in mind, includes a number of parameters, such as classpath, that make sense only for Java execution and that we wish to suppress, as well as a few more general parameters such as working directory that we also do not have any use for. PropertyDescriptor.setHidden( ) is used to ensure that these properties do not appear to the user. We leave untouched the inherited externalExecutor property that should appear. An alternate approach would be to specify no inherited bean info and just provide the externalExecutor property and the name property inherited from ServiceType.

Registering the Players as Services

Now that the audio players have been created, they need to be registered so that the IDE can find them when requested. An older method of registration involves simply listing them in the module manifest. This is easy to do. However, it is also inefficient during startup of the IDE—every such service needs to be loaded into memory, including its BeanInfo, typically also loading a network of related classes, which contributes to bloating of the virtual machine with possibly unused classes and objects. Instead, we register XML files into the module’s XML layer indicating the presence of such services and let them be loaded if and when they are needed. Make the following changes to your layer.xml document:

<folder name="Services">
  <folder name="Executor">
    <folder name="org-netbeans-examples-modules-minicomposer">
      <attr name="SystemFileSystem.localizingBundle"
            stringvalue="org.netbeans.examples.modules.minicomposer.Bundle"/>
      <attr name="SystemFileSystem.icon" urlvalue=
      "nbresloc:/org/netbeans/examples/modules/minicomposer/ScoreDataIcon.gif"/>
      <file name="internal-player.settings" url="internal-player.xml">
        <attr name="SystemFileSystem.localizingBundle"
              stringvalue="org.netbeans.examples.modules.minicomposer.Bundle"/>
        <attr name="SystemFileSystem.icon" urlvalue=
 "nbresloc:/org/netbeans/examples/modules/minicomposer/InternalPlayerIcon.gif"/>
        <attr name="SystemFileSystem.layer" stringvalue="project"/>
      </file>
      <file name="external-player.settings" url="external-player.xml">
        <attr name="SystemFileSystem.localizingBundle"
              stringvalue="org.netbeans.examples.modules.minicomposer.Bundle"/>
        <attr name="SystemFileSystem.icon" urlvalue=
 "nbresloc:/org/netbeans/examples/modules/minicomposer/ExternalPlayerIcon.gif"/>
        <attr name="SystemFileSystem.layer" stringvalue="project"/>
      </file>
    </folder>
  </folder>
</folder>
<folder name="Templates">
  <folder name="Services">
    <folder name="Executor">
      <folder name="org-netbeans-examples-modules-minicomposer">
        <attr name="SystemFileSystem.localizingBundle"
              stringvalue="org.netbeans.examples.modules.minicomposer.Bundle"/>
        <attr name="SystemFileSystem.icon" urlvalue=
      "nbresloc:/org/netbeans/examples/modules/minicomposer/ScoreDataIcon.gif"/>
        <file name="external-player.settings" url="external-player.xml">
          <attr name="SystemFileSystem.localizingBundle"
               stringvalue="org.netbeans.examples.modules.minicomposer.Bundle"/>
          <attr name="SystemFileSystem.icon" urlvalue=
 "nbresloc:/org/netbeans/examples/modules/minicomposer/ExternalPlayerIcon.gif"/>
          <attr name="template" boolvalue="true"/>
        </file>
      </folder>
    </folder>
  </folder>
</folder>

You also need to add these lines to your Bundle.properties file:

Templates/Services/Executor/org-netbeans-examples-modules-minicomposer=
  Audio Players
Templates/Services/Executor/org-netbeans-examples-modules-minicomposer/
  external-player.settings=External AU Player
Services/Executor/org-netbeans-examples-modules-minicomposer=
  Audio Players
Services/Executor/org-netbeans-examples-modules-minicomposer/
  external-player.settings=External AU Player
Services/Executor/org-netbeans-examples-modules-minicomposer/
  internal-player.settings=Internal AU Player

Now create XML documents for the internal and external players. internal-player.xml is shown in Example 24-5, and external-player.xml is shown in Example 24-6.

Example 24-5. Minicomposer: src/org/netbeans/examples/modules/minicomposer/internal-player.xml

<!DOCTYPE settings PUBLIC
          "-//NetBeans//DTD Session settings 1.0//EN"
          "http://www.netbeans.org/dtds/sessionsettings-1_0.dtd">
<settings version="1.0">
  <module name="org.netbeans.examples.modules.minicomposer"/>
  <instanceof class="org.openide.execution.Executor"/>
  <instanceof
    class="org.netbeans.examples.modules.minicomposer.InternalPlayer"/>
  <instance class="org.netbeans.examples.modules.minicomposer.InternalPlayer"/>
</settings>

Example 24-6. Minicomposer: src/org/netbeans/examples/modules/minicomposer/external-player.xml

<!DOCTYPE settings PUBLIC
          "-//NetBeans//DTD Session settings 1.0//EN"
          "http://www.netbeans.org/dtds/sessionsettings-1_0.dtd">
<settings version="1.0">
  <module name="org.netbeans.examples.modules.minicomposer"/>
  <instanceof class="org.openide.execution.Executor"/>
  <instanceof
    class="org.netbeans.examples.modules.minicomposer.ExternalPlayer"/>
  <instance class="org.netbeans.examples.modules.minicomposer.ExternalPlayer"/>
</settings>

First, we need to register the players as services. While they could in principle be placed anywhere in the Services/ folder and be found, in order to appear in the UI of the Options window, they should be placed beneath Services/Executor/ alongside other executors. In fact, we created a subfolder org-netbeans-examples-modules-minicomposer to hold the players, to better group them in the UI.

Each service is represented by one *.settings file, a simple XML format that declares the class of the service, as well as its important superclasses (or interfaces).

Note

Here we name the physical sources that will be put in the module JAR *.xml since the contents are in an XML format and common source editors will recognize that extension and treat the file as XML. Nonetheless, the virtual file in the system filesystem must be named *.settings, wherever the contents come from. Thus we have layer entries such as

<file name="external-player.settings" url="external-player.xml">...</file>

Anyone asking the Lookup API for an Executor will be offered both services. Anyone asking for an ExternalPlayer will be offered the external player only (Lookup was discussed in Chapter 17). User customizations to the service will be written out in the same *.settings file, but on disk in the user directory. The module element ensures that settings are uninstalled cleanly with the module, even if there is a modified *.settings file on disk.

The addition of SystemFileSystem.localizingBundle and SystemFileSystem.icon control the display name and icon of the subfolder and services. If you use manifest-based registration, these are automatic for the services, because the BeanInfo is loaded; we wish to avoid that, so these are specified as attributes of the settings XML files. Thus the services can be pleasantly displayed without even loading the classes involved, until the Properties window needs to display per-service properties.

We also add some entries under Templates/Services/ in a folder structure mirroring that under Services/. This causes the external player to be listed as a template that the user can construct more of. Perhaps the operating system has several audio players which are usable: by creating additional external player services from a template, it is possible for the user to make a service for each such program, and switch between them quickly. We make no template for the internal player, because it has no user-configurable properties, so there is no purpose in having more than one instance of it.

Figure 24-1 is the result when the user opens the Options window looking for players.

Minicomposer: audio players in Options window

Figure 24-1. Minicomposer: audio players in Options window

Creating Player Configuration Support

It is fairly straightforward to replace the original fixed ExecCookie implementation with a switchable executor, as shown in Example 24-7. org.openide.loaders.ExecSupport provides a standardized way of binding a user-configurable execution service to a data object. It also handles debugger types as an implementation of DebuggerCookie, though we will not use this ability, and the very similar CompilerSupport provides a switchable implementation of the CompilerCookie subinterfaces.

Example 24-7. Minicomposer: src/org/netbeans/examples/modules/minicomposer/ScoreExecSupport.java

public class ScoreExecSupport extends ExecSupport {
  public ScoreExecSupport(MultiDataObject.Entry entry) {
    super(entry);
  }
  protected Executor defaultExecutor( ) {
    return ComposerSettings.getDefault( ).getPlayer( );
  }
}

You then need to add the following lines to your ScoreDataObject’s constructor:

  public ScoreDataObject(FileObject pf, ScoreDataLoader loader)
      throws DataObjectExistsException {
    super(pf, loader);
    CookieSet cookies = getCookieSet( );
    EditorCookie ed = new ScoreEditorSupport(this);
    cookies.add(ed);
    cookies.add(new ScoreSupport(this, ed));
    cookies.add(new ScoreOpenSupport(getPrimaryEntry( )));
    cookies.add(new ScoreCompilerSupport.Compile(this));
    cookies.add(new ScoreCompilerSupport.Build(this));
    cookies.add(new ScoreCompilerSupport.Clean(this));
    // ScoreExec replaced by:
    cookies.add(new ScoreExecSupport(getPrimaryEntry( )));
  }

The ScoreExecSupport is a simple subclass of the base class, which is initialized via the primary entry from the object (a representation of the *.score file, in this case). All it must do is override defaultExecutor( ) to provide a default for the score’s player where none has been explicitly configured by the user.

Using the ExecSupport class only guarantees that an ExecCookie will be available on the data object and that it will look for an explicit choice of executor (player). It does this lookup based on a file attribute of the data object’s primary file: NetBeansAttrExecutor, which should be set to a ServiceType.Handle, a lightweight serializable handle for services that stores the identifying name (usually display name) and class of the service. Actually providing a UI for the user to configure the executor requires a second step, in ScoreDataNode:

  protected Sheet createSheet( ) {
    Sheet sheet = super.createSheet( );
    ExecSupport support = (ExecSupport)getCookie(ExecSupport.class);
    Sheet.Set set = new Sheet.Set( );
    set.setName("execution");
    set.setDisplayName(NbBundle.getMessage(ScoreDataNode.class,
                                           "LBL_Execution"));
    set.setShortDescription(NbBundle.getMessage(ScoreDataNode.class,
                                                "HINT_Execution"));
    support.addProperties(set);
    set.remove(ExecSupport.PROP_DEBUGGER_TYPE);
    set.remove(ExecSupport.PROP_FILE_PARAMS);
    sheet.put(set);
    return sheet;
  }

We create an additional tab in the property sheet for score data nodes, labeled Execution, and use ExecSupport.addProperties( ) to include several properties in it that the support knows how to bind to the primary file using attributes. In fact addProperties( ) includes more properties than we need—Debugger and Arguments—so we simply remove the extraneous ones, as there is no expectation of a score being “debuggable” nor requiring additional per-score arguments to be played.

Figure 24-2 is a score file showing the ability to change its player, and Figure 24-3 is the custom Property Editor that permits detailed configuration, including selection of the process itself (in Figure 24-4).

Minicomposer: choosing to use an external audio player

Figure 24-2. Minicomposer: choosing to use an external audio player

Minicomposer: configuring an external audio player

Figure 24-3. Minicomposer: configuring an external audio player

Minicomposer: selecting the audio player process

Figure 24-4. Minicomposer: selecting the audio player process

Creating a SystemOption for the Default Executor

In the previous section, we saw how to permit the user to select a player for a particular score. More commonly, a user will select a certain player as the default for all score files. The ScoreExecSupport can supply a default player where none is explicitly given, that is, where the primary *.score file has no NetBeansAttrExecutor file attribute. Now we need a GUI for the user to select this default and persist that choice.

This can be done using a system option (see Example 24-8), the standard means in the NetBeans APIs of storing a single property or cluster of them and providing a matching GUI. While we are making an option with a default player property, we will also introduce an additional property for the sample rate to use when compiling scores to audio files—this was previously hard-coded in Chapter 23.

Example 24-8. Minicomposer: src/org/netbeans/examples/modules/minicomposer/ComposerSettings.java

public class ComposerSettings extends SystemOption {
  public static final String PROP_PLAYER = "player";
  public static final String PROP_SAMPLE_RATE = "sampleRate";
  private static final long serialVersionUID = -1247005365478408406L;
  public Executor getPlayer( ) {
    ServiceType.Handle val = (ServiceType.Handle)getProperty(PROP_PLAYER);
    Executor exec = null;
    if (val != null) {
      exec = (Executor)val.getServiceType( );
    }
    if (exec == null) {
      if (Utilities.isWindows( ) ||
          Utilities.getOperatingSystem( ) == Utilities.OS_SOLARIS) {
        exec = Executor.find(InternalPlayer.class);
      } else {
        exec = Executor.find(ExternalPlayer.class);
      }
    }
    if (exec == null) {
      exec = new InternalPlayer( );
    }
    return exec;
  }
  public void setPlayer(Executor player) {
    putProperty(PROP_PLAYER, new ServiceType.Handle(player), true);
  }
  public String displayName( ) {
    return NbBundle.getMessage(ComposerSettings.class, "LBL_ComposerSettings");
  }
  public HelpCtx getHelpCtx( ) {
    return new HelpCtx("org.netbeans.examples.modules.minicomposer");
  }
  public static final ComposerSettings getDefault( ) {
    return (ComposerSettings)findObject(ComposerSettings.class, true);
  }
  public float getSampleRate( ) {
    Float val = (Float)getProperty(PROP_SAMPLE_RATE);
    if (val != null) {
      return val.floatValue( );
    } else {
      return 24000.0f;
    }
  }
  public void setSampleRate(float sampleRate) {
    putProperty(PROP_SAMPLE_RATE, new Float(sampleRate), true);
  }
}

Properties stored in system options are given as bean properties—this controls both the serialization of the option, which is automatically done one property at a time for robustness in case of deserialization failures, and the display of the properties in the Options window.

getPlayer( ) looks up the current setting for the player property. This system option uses the key-value storage available to every SharedClassObject (of which SystemOption is a subclass) to actually store the properties. getProperty( ) gets a value by name. Here the option actually stores ServiceType.Handles, which as previously mentioned retain the name and class name of the service—the actual instance fields of the service are thus stored only in the pool of services, and the handle points to a member of the pool. In case the value has never been set, we make the internal player the default on Windows and Solaris machines, whereas others (for example, Linux) get an external player by default. Note that Executor.find( ) could return null in case the user has for some reason deleted every instance of a service class, so for safety the last fallback is to create a fresh internal player.

setPlayer( ), the setter method corresponding to getPlayer( ), stores a handle to the executor. The final argument true to putProperty( ) requests that a property change event also be fired from the system option (here with property name PROP_PLAYER), which is necessary for GUI updates and any code that might be listening for a change in the default.

displayName( ) and getHelpCtx( ) provide a display name for the option and a link to JavaHelp, respectively. getDefault( ) is a convenience method for client code to find the singleton instance of the option. Recall that ScoreExecSupport used this method to get the option instance, followed by getPlayer( ) to find a default player to use.

We also define an additional property with the name PROP_SAMPLE_RATE and default value 24000 (samples per second). For this to be used, it is only necessary to replace the line in SampledAudioCompilerGroup:

AudioInputStream ais =
  new LineInFromScore.ScoreAudioInputStream(line, 24000.0f);

This can be changed to the following:

AudioInputStream ais =
  new LineInFromScore.ScoreAudioInputStream(line,
    ComposerSettings.getDefault( ).getSampleRate( ));

A similar change can be made in the LineInFromScore(Score) constructor to not hard-code the sample rate. LineInFromScore.java is not listed here, so check the downloadable sources for the example if you are interested.

If the option is left without a bean info, it will be usable but unattractive to the user: the internal unlocalized property names player and sampleRate will be visible. So we give it a simple bean info, shown in Example 24-9.

Example 24-9. Minicomposer: src/org/netbeans/examples/modules/minicomposer/ComposerSettingsBeanInfo.java

public class ComposerSettingsBeanInfo extends SimpleBeanInfo {
  public PropertyDescriptor[ ] getPropertyDescriptors( ) {
    ResourceBundle bundle = NbBundle.getBundle(ComposerSettingsBeanInfo.class);
    try {
      PropertyDescriptor player =
        new PropertyDescriptor("player", ComposerSettings.class);
      player.setDisplayName(bundle.getString("PROP_player"));
      player.setShortDescription(bundle.getString("HINT_player"));
      PropertyDescriptor sampleRate =
        new PropertyDescriptor("sampleRate", ComposerSettings.class);
      sampleRate.setDisplayName(bundle.getString("PROP_sampleRate"));
      sampleRate.setShortDescription(bundle.getString("HINT_sampleRate"));
      sampleRate.setExpert(true);
      sampleRate.setPropertyEditorClass(SampleRateEd.class);
      return new PropertyDescriptor[ ] {player, sampleRate};
    } catch (IntrospectionException ie) {
      TopManager.getDefault( ).getErrorManager( ).notify(ie);
      return null;
    }
  }
  public Image getIcon(int type) {
    if (type == BeanInfo.ICON_COLOR_16x16 || type == BeanInfo.ICON_MONO_16x16) {
      return Utilities.loadImage(
        "org/netbeans/examples/modules/minicomposer/ScoreDataIcon.gif");
    } else {
      return null;
    }
  }
  public static class SampleRateEd extends PropertyEditorSupport {
    private static final float[ ] rates = new float[ ] {
      12000.0f, 24000.0f, 48000.0f
    };
    private static final String[ ] tags = new String[rates.length];
    static {
      NumberFormat format = new DecimalFormat( );
      for (int i = 0; i < rates.length; i++) {
        tags[i] = format.format(rates[i]);
      }
    }
    public String[ ] getTags( ) {
      return tags;
    }
    public String getAsText( ) {
      float value = ((Float)getValue( )).floatValue( );
      for (int i = 0; i < rates.length; i++) {
        if (rates[i] == value) {
          return tags[i];
        }
      }
      return "???";
    }
    public void setAsText(String text) throws IllegalArgumentException {
      for (int i = 0; i < tags.length; i++) {
        if (tags[i].equals(text)) {
          setValue(new Float(rates[i]));
          return;
        }
      }
      throw new IllegalArgumentException( );
    }
  }
}

The bean info is uninteresting except for the custom property editor attached to the sampleRate property. This editor supplies three standard sample rates common in audio files and permits one of them to be selected via a drop-down list. Note that the NetBeans infrastructure automatically supplies a property editor for the player property, as it is of type Executor—many commonly used types have standard property editors, which are listed in the Explorer API documentation. The Executor editor permits an existing executor to be selected from a drop-down list, and detailed properties of that editor can be configured from a custom editor dialog box.

To install the system option, we will eschew the older method of adding a line to the manifest, which is too eager to load its class, and instead install it via the XML layer (giving more control over UI as well). Add this fragment to your layer.xml document:

<folder name="Services">
  <file name="org-netbeans-examples-modules-minicomposer-option.settings"
        url="option.xml">
    <attr name="SystemFileSystem.localizingBundle"
        stringvalue="org.netbeans.examples.modules.minicomposer.Bundle"/>
    <attr name="SystemFileSystem.icon" urlvalue=
      "nbresloc:/org/netbeans/examples/modules/minicomposer/ScoreDataIcon.gif"/>
  </file>
</folder>
<folder name="UI">
  <folder name="Services">
    <folder name="IDEConfiguration">
      <folder name="ServerAndExternalToolSettings">
        <!-- Note: line break in file contents necessary: -->
        <file name="org-netbeans-examples-modules-minicomposer-option.shadow">
    <![CDATA[Services/org-netbeans-examples-modules-minicomposer-option.settings
SystemFileSystem
]]>
        </file>
      </folder>
    </folder>
  </folder>
</folder>

You will also need a settings file for the option, seen in Example 24-10.

Example 24-10. Minicomposer: src/org/netbeans/examples/modules/minicomposer/option.xml

<!DOCTYPE settings PUBLIC
          "-//NetBeans//DTD Session settings 1.0//EN"
          "http://www.netbeans.org/dtds/sessionsettings-1_0.dtd">
<settings version="1.0">
  <module name="org.netbeans.examples.modules.minicomposer"/>
  <instanceof class="org.openide.options.SystemOption"/>
  <instanceof
    class="org.netbeans.examples.modules.minicomposer.ComposerSettings"/>
  <instance
    class="org.netbeans.examples.modules.minicomposer.ComposerSettings"/>
</settings>

As with the players, we register the option as a *.settings file in Services/ (no particular subfolder is needed). It is again given a localized name and icon; the resource bundle should contain this line:

Services/org-netbeans-examples-modules-minicomposer-option.settings=
  Mini-Composer Settings

By default objects in the top-level Services/ folder are not visible in the GUI. Certain subfolders like Executor/ are explicitly made visible by NetBeans’ GUI configuration, for example, under the node Execution Types in the Options dialog box, but system options are not normally placed in such folders. To make the option appear, we choose a GUI category for it and create a data shadow pointing to its settings file (akin to a Unix symbolic link or Windows shortcut).

A shadow is a special kind of data object, org.openide.loaders.DataShadow, which delegates much of its appearance and behavior to its target. It has the .shadow extension and consists of two text lines: the first giving a resource path in a mounted filesystem, the second giving the programmatic system name of that filesystem.[16] The special filesystem controlling NetBeans configuration into which module XML layers are merged has the name SystemFileSystem, and the resource path matches what you see in the layer. We place this shadow beneath UI/Services/, which is the folder whose structure and contents directly generate the view seen in the Options window. Services/ has a program-friendly structure that must be kept stable between module versions in order for user-customized settings to continue to override module-supplied defaults. By contrast, UI/Services/ contains no real information except for the user-oriented GUI categories you see, and this structure can be freely rearranged between module or IDE releases.

The resulting option in the Options window looks like Figure 24-5. Although not displayed in the screenshot, the Expert tab holds the Sample Rate property.

Minicomposer: settings displayed in Options window

Figure 24-5. Minicomposer: settings displayed in Options window

The basic services of the Minicomposer are now complete. But since the NetBeans GUI is based heavily on the Explorer, we will next show how to fit scores naturally into the Explorer’s UI model.



[16] NetBeans 3.4 also permits a simpler format for *.shadow files based on file attributes.

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

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