The Source for Java Technology Collaboration
User: Password:
Register | Login help    

Search

Online Books:
java.net on MarkMail:


 E-mail  Print

Introducing JDesktop Integration Components, Part 2

Mon, 2004-11-01

{cs.r.title}





Contents
What is the SaverBeans SDK?
A Simple Example
A Custom Screensaver
Adapting an Existing Program
Further Work
Conclusion
Resources

"Write once, run anywhere." This has been the mantra of Java developers since day one. The philosophy has given us great flexibility and power, but always with trade-offs. Certain types of programming have effectively been off-limits to Java developers because of the speed requirements or access to native services. Now, finally, things are looking better. JIT virtual machines and Moore's Law have taken care of the speed improvements and the new JDesktop Integration Components finally let Java developers write programs that tie closely to the native operating system. Part one of this series covered the desktop, file, browser, and system tray components. In this article, we will explore an entirely new area, the SaverBeans SDK, which lets us write screen savers entirely in Java. We will go from a simple example to adapting a particle simulator with user-supplied configuration.

What is the SaverBeans SDK?

A screensaver seems like a simple thing: just fill the screen with a pretty animation and you're done. Sadly, nothing in the world of computers is that simple. A screensaver must be packaged in a certain way for the host operating system to recognize it and launch it at the correct time. And, of course, every operating system has its own way of doing this. Since JDIC was already doing such a great job of unifying disparate platform-specific APIs, it was only logical to tackle this one as well. The SaverBeans SDK is an incubator project that provides a cross-platform API for accessing the screen, along with a packager tool to produce platform-specific executables. SaverBeans doesn't come with any screensavers, but there is a secondary project where various java.net citizens have posted their own creations.

In this article, you will learn how to build a simple countdown screensaver; what to download, how to create your classes, and how to package it all up. You will also see how to take an existing program and convert it into a screensaver, including user configuration.

I should mention at this point that SaverBeans supports Windows, Linux, and Solaris, but not the Mac. The SaverBeans API uses native code underneath to do its magic, and no one has written a hook into the OS X screensaver API yet. If you know a little bit about native OS X programming, then please join the JDIC project and help out.

A Simple Example

Now that we have all of that out of the way, let's go make some screensavers. For the first one I've chosen something simple: a Christmas countdown timer. Every time you activate the screensaver, your computer will display the time left until Christmas morning, with the millisecond accuracy that anxious eight-year-olds demand.

First of all, you will need to download the SDK from the SaverBeans project page. This .zip file includes both the SDK itself and a sample screensaver you can compile and run. The native libraries have already been compiled so you won't need any native tools or C compilers to get started. You will need Ant and a recent JDK, though: 1.4 for Windows and 1.5 for Linux (due to some animation problems with 1.4 for Linux). Unzip this file and you will have a directory containing the main .jars, plus a second .zip file, saverbeans-startup.zip, which contains the sample screensaver.

I will only cover the Windows instructions here, but the documentation contains similar instructions for Linux. The only major difference is where you put the resulting screensaver executable.

Unzip the file and go into saverbeans-startup, and then copy the build.properties.sample file to build.properties. Edit the properties file and change the saverbeans.path property to point to where you have the saverbeans-ant.jar file (usually ..). Now type ant dist, which compiles the code, moves your program resources to platform directories (currently one each for Windows, Solaris, and Linux), then builds a custom executable (a .scr file, under Windows) that launches the screensaver class files. Copy the contents of dist/bouncingline-win32 to your C:/windows/system32 directory. Now head to the display properties control panel, and select the bouncingline screensaver. If all goes well, you will see a small preview in the mini-window of a bouncing line, as seen in Figure 1.

Figure 1. Bouncinglines preview
Figure 1. Bouncinglines preview

A Custom Screensaver

Now let's make our own! Here's the beginning:

package org.joshy.screensavers;

import org.jdesktop.jdic.screensaver.*;
import org.joda.time.*;
import java.awt.*;
import java.io.*;

public class XmasCountdown extends SimpleScreensaver {
    DateTime xmas;
    Duration old_dur;
    boolean first;
    
    public void init() {
        xmas = new DateTime(2004,12,25,5,40,0,0);
        DateTime now = new DateTime();
        old_dur = new Duration(DurationType.getDayHourType(),
                               now,xmas);
        first = true;
    }

The SaverBeans API is quite simple. Just subclass the SimpleScreensaver class in the org.jdesktop.jdic.screensaver package, and then override two methods. In the first method, init(), I have used a special DateTime class from the Joda project. Joda is a set of date and time utilities that replace the default Date and Calendar classes in Java. I chose to use Joda because it makes calculating the distance between two dates much easier than the standard Calendar class. In the init method, I created a timestamp for Christmas at 5:40 in the morning, which is about the time I used to get up as a child. I've also stored the initial distance between now and Christmas in a Duration object, old_dur. The Boolean, first, tells the paint method that it has just been initialized so that it knows to redraw the entire screen. The paint method is below:

public void paint(Graphics g) {
    DateTime now = new DateTime();
    Duration dur =
        new Duration(DurationType.getDayHourType(),
                     now,xmas);
    
    // get the screen size and clear the screen
    Component c = getContext().getComponent();
    int w = c.getWidth();
    int h = c.getHeight();
    
    ((Graphics2D)g).setRenderingHint(
        RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_ON);
    Font font = new Font("SansSerif",Font.PLAIN,50);
    g.setFont(font);
    
    // draw the header text
    g.setColor(Color.green);
    if(first) { g.drawString("Countdown to Christmas",10,50); }
                
    drawText(g,"Days: ", dur.getDays(), 
        old_dur.getDays(),150);
    drawText(g,"Hours: ", dur.getHours(), 
        old_dur.getHours(),200);
    drawText(g,"Minutes: ", dur.getMinutes(), 
        old_dur.getMinutes(),250);
    drawText(g,"Seconds: ", dur.getSeconds(), 
        old_dur.getSeconds(),300);
    drawText(g,"MilliSeconds: ", dur.getMillis(), 
        old_dur.getMillis(),350);
    
    old_dur = dur;
    first = false;
}

I'll go through this step by step. The first two lines calculate the distance between the current time and Christmas. Next, it pulls out a reference to the drawing component via the context and calculates the width and height of the screen. After that it turns on anti-aliasing, sets up a large font, and then draws the title "Countdown to Christmas" in green. The title is only drawn on the first pass so that we don't have to erase and redraw it each time. The rest of the paint method draws each part of the time until Christmas on its own line in red using the drawText method. Finally we update the old_dur and first variables for the next time paint() is called.

To get this class running as a screensaver, we have to tell Ant about our new class and .jar. Start by modifying the screensaver and screensaver.class properties at the top of the build.xml file.

If you need extra .jars for your screensaver, like mine does, then things get a bit tricker. Due to a limitation in the launching system, a SaverBeans screensaver cannot specify a complete classpath. Instead, it hands Java the name of a .jar from which to load the main class. I modified the manifest of my main .jar to automatically include the support .jars, like this:

Manifest-Version: 1.0
Main-Class: org.joshy.screensaver.XmasCountdown
Class-Path: joda-time-0.95.jar

To get this manifest into the .jar, I modified the jar task in the dist target like this:

<jar destfile="${build}/jar/${screensaver}.jar"
        manifest="src/conf/MANIFEST.MF">
    <fileset dir="${build}/share" />
</jar>;

There is only one last thing to do. Each screensaver has a config file, stored in the src/conf directory. I copied the default one into xmascountdown.xml and changed the .jars and classes to match the new screensaver.

<?xml version="1.0" encoding="UTF-8"?>

<screensaver name="xmascount" 
    _label="Countdown to Christmas">
    
    <command arg="-root"/>
    <command arg="-jar xmascount.jar"/>
    <command arg="-cp joda-time-0.95.jar;xmascount.jar"/>
    <command 
    arg="-class org.joshy.screensavers.XmasCountdown"/>
    <file id="jdkhome" _label="Java Home (blank=auto)" 
    arg="-jdkhome %" />

    <_description> This screensaver counts down,
    with great precision, the amount of time left until
    the magic day, defined as just a hair after
    midnight Christmas Eve. </_description>

</screensaver>

Some of the elements may look a little strange, like the _description, for example. That is because this is actually the configuration file for XScreenSaver, the gold standard of Unix screensaver programs. Under Linux and Solaris, this file is used directly to launch the program and create a settings screen. Since Windows just uses .scr executables, SaverBeans will parse this and generate a settings screen internally. Though this part is a bit complicated, for the most part, you can set it up once and forget about it.

Now run ant clean dist again and your new screensaver will be placed in the dist/xmascount-win32 directory. Copy the contents (xmascountdown.scr and the three .jar files) to C:/windows/system32 and your screensaver is ready to go. It should look something like Figure 2.

Figure 2. Countdown To Christmas Screensaver
Figure 2. Countdown To Christmas Screensaver

Adapting an Existing Program

Well, that wasn't so bad, and once you do it the first time it becomes pretty easy to manage it all with the Ant file. Now let's try something more complicated: turning an existing application into a screensaver.

In a previous article, "Getting Started with Scripting," I built a simple particle simulator. It used a mixture of JavaScript and Java code to create colorful displays on the screen. What better way to show off those displays then to create a screensaver for them?

The SimEditor class is essentially a shell containing a SimView to display the graphics, a JavascriptProvider to create the code buffers that draw each frame, and a SimRunner to control the animation in a thread. Turning this into a screensaver will be a simple exercise in refactoring.

To ensure our changes have as little impact on the codebase as possible, I'm going to try to keep all of the new code in a single class, SimScreenSaver. As with the Christmas counter, we need to make it a SimpleSaver subclass. Since we won't have an onscreen editor to type in the JavaScript, SimScreenSaver will have to be a JavascriptProvider, too. So here's what we have so far:

public class SimScreenSaver 
    extends SimpleScreensaver
    implements JavascriptProvider {
    
    SimRunner runner;
    SimFrame frame;
    public String init, update, draw;
    SimView view;    

    public void init() {
        
        SimContext ctx = new SimContext();
        ctx.setBlur(true);
        ctx.setRadialStretch(true);
        ctx.setMaxParticles(20);
        
        load("circle.xml");

        
        runner = new SimRunner();
        runner.setContext(ctx);
        context.setRunner(runner);
        
        // setup the javascript provider frame and runner
        frame = new JavascriptFrame(this);
        runner.setFrame(frame);
        runner.setup();
        runner.initParticles();
        
        // prepare the view
        view = new SimView();
        int w = getContext().getComponent().getWidth();
        int h = getContext().getComponent().getHeight();
        view.setSize(w,h);
        view.setContext(ctx);
    }
    
    public String getInit() {
        return init;
    }
    public String getUpdate() {
        return update;
    }
    public String getDraw() {
        return draw;
    }
}

At the top there is a reference to each piece we will need: a SimRunner, a SimFrame, and a SimView. In the init method, we create a SimContext to tie each component together, and to hold the display settings (like the maximum number of particles).

The load method (the implementation of which is not shown above) loads an XML file containing each snippet of Java code into the init, update, and draw code buffers. It loads the XML from the classpath so the user doesn't have to worry about about where to install the XML files.

At then end of the init method, you can see that we call getContext() and then getComponent(). This is to get a reference to the actual drawing canvas that fills the screen when the saver is activated. Using the size of this canvas, we can set the size of the view. It is important not to hard code the screen dimensions, because the end user's computer might be running at a different resolution or the saver might be in preview mode (which is often quite tiny).

After the init method is the implementation of JavascriptProvider that returns the code buffers for the init, update, and draw phases of the simulator.

Now that the screensaver is set up, we have to actually draw it. Normally, we would call runner.start() to kick off the animation, but we don't want to do that here. The SaverBeans framework has its own animation thread that will call the SimScreenSaver's paint method at the right time, so we don't need to use the SimRunner's threading. However, we do want to reuse its loop method that launches the Java interpreter and paints the screen. The only problem is that if we just called runner.loop(), it would block forever. The method loop is synchronized because it was originally intended for use inside the runner's animation thread. To get around, this I have created a second loop method called loopUnSynchronized() that does the same thing, but without the synchronized keyword and the wait() call. This way, we can use whichever form of looping is appropriate for the situation.

After the runner has looped, we just want to paint the view. The view is a JPanel, but it hasn't been added to a JFrame and been realized on screen. However, since we are running inside of the SaverBeans framework, we don't need all of its Swing code. Instead, we just reuse its paintComponent() method to draw the particles onscreen. Here is the new SimScreenSaver paint method:

    public void paint(Graphics g) {
        runner.loopUnsynchronized();
        view.paintComponent(g);
    }

With this class, we can build a new screensaver like we did before and drop the SimSaver.scr file into the system directory. The running screensaver is shown in Figure 3.


Figure 3. SimScreenSaver running Circles

The simulator works. There's just one problem though. Using SimEditor, the user could choose which sim to run and turn on the blurring, but these values are hard coded in SimScreenSaver. It would be nice to provide a control panel for the user to change these settings. Fortunately, the SaverBeans framework provides a way to do this as well. You may recall that the configuration XML is actually an XScreenSaver definition file. In addition to holding startup parameters, the file can also define user-selectable settings. The framework will automatically create a settings panel for the user and then pass the selected settings to the screensaver. Under Windows, this is done with a special Java application. Under Linux, the settings screen is created by XScreenSaver itself.

Here is our new config file:

<?xml version="1.0" encoding="UTF-8"?>
<screensaver name="SimSaver" 
  _label="Javascript Particle Simulator Screensaver">
  <command arg="-root"/>  
  ...
  
  <!-- start settings definitions -->
  <select id="blur">
    <option id="on" _label="Blur On" 
    arg-set="-blur on" />
    <option id="off" _label="Blur Off" 
    arg-set="-blur off" />
  </select>

  <select id="radial">
    <option id="on" _label="Radial Stretch On" 
    arg-set="-radial on" />
    <option id="off" _label="Radial Stretch Off" 
    arg-set="-radial off" />
  </select>

  <select id="simfile">
    <option id="circle.xml" _label="Simple Circle" 
    arg-set="-simfile circle.xml" />
    <option id="swirl.xml"   _label="Eccentric Circle" 
    arg-set="-simfile swirl.xml" />
    <option id="falling.xml" _label="Falling Lines" 
    arg-set="-simfile falling.xml" />
    <option id="volcano.xml" _label="Volcano" 
    arg-set="-simfile volcano.xml" />
    <option id="raindrops.xml" _label="Rain Drops" 
    arg-set="-simfile raindrops.xml" />
  </select>
  
</screensaver>

The config file above defines three select values: blur, radial, and simfile, which will be displayed as three drop-down menus to the user. The id of the select is the key we can use later to retrieve the selected value. Each select has several options whose ids define the possible values. The _label attribute is the text of the option that will be displayed to the user. The arg-set defines the options that will be passed to XScreenSaver under Linux, so those should match the select and option ids. For example, the first select is blur and its first option is on, so the arg-set for the first option would be -blur on. With these settings in the config file, we will get a settings panel, the same as when the user selects settings in the screensaver control panel. The settings panel is shown in Figure 4.

Figure 4. SimSaver settings screen
Figure 4. SimSaver settings screen

Back in our SimScreenSaver class, we need to retrieve the current settings, providing defaults if the settings are missing (i.e., if the user didn't select anything). The modified init() method looks like this:

public void init() {
    ScreensaverSettings settings = getContext().getSettings();
    
    SimContext ctx = new SimContext();
    
    // setup blur
    String blur = settings.getProperty("blur");
    if(blur == null || blur.equals("on")) {
        ctx.setBlur(true);
    } else {
        ctx.setBlur(false);
    }
    
    // setup radial stretch
    String radial = settings.getProperty("radial");
    if(radial == null || radial.equals("off")) {
        ctx.setRadialStretch(false);
    } else {
        ctx.setRadialStretch(true);
    }
    
    ctx.setMaxParticles(20);
    
    // setup and load the sim file
    String file = settings.getProperty("simfile");
    if(file == null) {
        file = "circle.xml";
    }
    load(file);
    ... rest of init method
}

First we have to retrieve the ScreensaverSettings object from the context with getContext().getSettings(). Next we test the value of each property (checking for null, of course) and set the appropriate value in the SimContext, ctx. With that, users can change the screensaver to their hearts' content.

In addition to the sims from the original scripting article, I have included a new sim called Rain Drops. It draws ever-expanding circles that combine with the blur effect to look like Figure 5:

Figure 5. Raindrops screensaver
Figure 5. Raindrops screensaver

Further Work

If you are interested in playing with the simulator more, you can find the entire source on my web site here. The SaverBeans framework is here and you can find tons of screensavers built by other developers. I recommend browsing the screensavers forum to find tips and tricks on building your own screensavers.

Conclusion

As part of the JDIC project, SaverBeans provides a framework for building a type of Java application never before possible, and, as always, making it cross-platform. In the few months since its release, JDIC has proven that there is pent-up demand for doing interesting things with desktop Java. As it matures, I hope JDIC will continue to grow and provide us with the kind of robust feature set we expect from Java.

Resources

Joshua Marinacci first tried Java in 1995 at the request of his favorite TA and has never looked back.
Related Topics >> GUI      
Comments
Comments are listed in date ascending order (oldest first)