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.
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.
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 InternalPlayer
s
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:
Recall that Utilities.loadImage( )
loads images
from module
JARs and does caching.
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
IOException
s 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 NbProcessDescriptor
s, 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
.
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).
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.
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).
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.Handle
s, 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.
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.
18.219.103.183