Technical: Java
Advanced Search
Apple Developer Connection
Member Login Log In | Not a Member? Contact ADC

title


Previous Section Table of Contents Next Section


Module 5- Custom Media Controllers in QTJ Part 2

Contents

Overview

Introduction to QuickTime Controllers

  1. Setting up the Project

  2. Designing the Buttons

  3. Laying the Foundation

  4. Modifying AnimalPane's Inheritance

  5. Further Organization

  6. Adding Image Loading Utility Code

  7. Loading the Controller Media

  8. Creating a Custom Button Class

  9. Creating the Buttons

  10. Creating a Button Listener Class

  11. Registering the Action Listeners

  12. Creating a Controller Object

  13. Registering the Buttons with the Controller

  14. Implementing the Start Method

  15. Implementing the Stop Method

  16. Final Modifications to the Zoo Class

Summary

Further Exploration


Creating a Custom Button Class

QuickTime has two built-in button classes that can be used for custom controllers - Our custom controller is fairly modal. Each button is aware of the overall state of the controller. For example, when the play button is the active button, it has a little blue indicator on it.

QuickTime provides us with three built-in button classes that are all derrived from the QTButton class. These three classes, PressActionButton, PressReleaseButton, and ReleaseButton all behave in a slightly different manner.

The PressActionButton fires an event when it is first pressed and continues to fire an event until it is released. This is great for a button that has a periodic action such as a scroll bar arrow. The scroll bar continues to scroll as long as you hold down the button (or until the end of the bar is reached).

The PressReleaseButton fires an event when it is pressed and also when it is released. This type of button is good to represent a toggle button. For example, if you had a mute button that muted audio as long as it was held down, you would use this button class. The audio state would be toggled when the button was pressed (from on to off) and then toggled back when the button was released.

The last class, the ReleaseButton only sends an event when the button is released. This is the most general purpose button class and is the type we will be using for our controller.

When you create a ReleaseButton, you specify the default image, the pressed image, and the deactive image. You may also specify an additional rollover image, if you wish. When you specify these images, the button class automatically handles responding to the user input and swapping the images based on the user interaction. This is great for most button types, but is not very good for our custom button which has three states: Active, Pressed, and Inactive. The ReleaseButton deactiveImage parameter is designed to be used when the entire window is deactivated. This is not the correct behaviour for our button. We need to be able to toggle both the play button and the stop button between the active and inactive states based on the status of the movie.

In order to do this, we need to create a new class and derive it from ReleaseButton. Create a new java source file and name it MovieButton.java. The source for the file is the following:

MovieButton.java
 import quicktime.QTException;
import quicktime.app.ui.ReleaseButton;
import quicktime.app.image.ImageSpec;


public class MovieButton extends ReleaseButton { MovieButton(ImageSpec releasedImage, ImageSpec pressedImage, ImageSpec deactiveImage) throws QTException { super(releasedImage, pressedImage, deactiveImage); } public void setCurrentImage(ImageSpec image) throws QTException { super.setCurrentImage(image); } }

After importing the appropriate classes, we declare a public class MovieButton that derives from ReleaseButton. Our constructor simply calls the constructor of the superclass and passes the appropriate parameters, and we override setCurrentImage( ) from the UIElement class. We override this method because it is protected, and we need to make it public so that we can change the default button image to our active or inactive state. The implementation simply calls our inherited method.

Back to top

Creating the Buttons

Now that we have created our own button class that makes the methods we need accesible, it's time to use them:

AnimalPane.java
    ...

public class AnimalPane extends ZooPane
{
    ...
public void playSound() { ... }
protected void addMovie( String moviePath, int layer, int x, int y ) { Movie m = makeMovie( new QTFile( QTFactory. findAbsolutePath( moviePath ))); md = new MoviePresenter( m ); md.setLocation(x, y); playRel = makePresenterFromFile( "data/Play.jpg" ); playPress = makePresenterFromFile( "data/PlayPressed.jpg" ); playPlaying = makePresenterFromFile( "data/Playing.jpg");

        playButton = new MovieButton( playRel, playPress,
                    playPlaying );
        playButton.setLabel("Play");
        playButton.setLocation( x + 68, y + 152 );

        stopRel     = makePresenterFromFile( "data/Stop.jpg" );
        stopPress   = makePresenterFromFile( 
                         "data/StopPressed.jpg" );
        stopDeactive= makePresenterFromFile("data/Stopped.jpg");
        stopButton  = new MovieButton( stopRel, stopPress, 
            stopDeactive );
        stopButton.setLabel("Stop");
        stopButton.setLocation( x + 136, y + 152 );
 
rewRel = makePresenterFromFile( "data/Rewind.jpg" ); rewPress = makePresenterFromFile( "data/RewindPressed.jpg" ); rewDeactive = rewRel;
        rewindButton = new MovieButton( rewRel, rewPress, 
            rewDeactive );
        rewindButton.setLabel( "Rewind" );
        rewindButton.setLocation( x, y + 152 );

compositor.addMember( md, 1ayer );
        compositor.addMember(playButton);
        compositor.addMember(stopButton);
        compositor.addMember(rewindButton);

        compositor.getTimer().setRate(1); 
        md.setRate(1);
} 

        ... 
    protected QTPlayer player;
    protected MoviePresenter md;

    protected ImagePresenter playRel;
    protected ImagePresenter playPress;
    protected ImagePresenter playPlaying; 
    protected MovieButton playButton;
    
protected ImagePresenter stopRel; protected ImagePresenter stopPress; protected ImagePresenter stopDeactive;
    protected MovieButton stopButton;
    
protected ImagePresenter rewRel; protected ImagePresenter rewPress; protected ImagePresenter rewDeactive;
    protected MovieButton rewindButton;
}

This section of code modifications seems long, but is actually straightforward. Don't be alarmed! We will start at the bottom of the AnimalPane source file and declare the three button objects. These objects are instances of our own custom class, and we declare one for each of our buttons: play, stop, and rewind.

Next, for each of the buttons, we perform three basic steps:

  1. Create the button object by calling the constructor with the three image presenter objects containing the image for each button state.
  2. Set the label of the button to a string value that makes sense. This will allow us to uniquely identify each button. For example, if we got an action event, we could tell which button the event was from by checking the label of the button the even originated from.
  3. Offset the button within the compositor so that it is in its proper location.

Finally, we add each button to the compositor. Now that our buttons are created, we need to "wire them" to our movie so that they work correctly when the user clicks on them. We will do this over the next several steps.

Back to top

Creating a Button Listener Class

Our buttons fire an action event when they are pressed. In order to respond to these action events, we must create a ButtonListener class:

AnimalPane.java
    ...

public class AnimalPane extends ZooPane
{
    public AnimalPane()
    {
        ...
    }
    public class ButtonListener implements QTActionListener	
    {
        public void actionPerformed( QTActionEvent event ) 
        {
            try
            {
                // body goes here
            }
            catch( QTException exc )
            {
                exc.printStackTrace();
            }
        }
    }
 
       ...
}

 

In order to handle controller related action events (QTActionEvents), we need to implement the QTActionListener interface. We create a public inner class called ButtonListener that implements this interface. As is customary with classes derrived from EventListener, we implement a single method called actionPerformed that takes a QTActionEvent parameter. Once we register this listener with the buttons, we will receive notification of any QTActionEvents that are broadcast from the MovieButton objects.

We set up a try / catch block in the body of our action performed method in anticipation of intercepting any exceptions that will be thrown in this method. In our next step we will implement the actionPerformed method:

AnimalPane.java
    ...

    public class ButtonListener implements QTActionListener	
    {
        public void actionPerformed( QTActionEvent event ) 
        {
            try
            {
                Object source = event.getSource();
				
                if( source.equals( playButton ))  
                {
                    if( md.getRate() == 0 )
                    {
                        md.setRate( 1 );
                    }
                    stopButton.setCurrentImage( stopRel );
                    stopButton.setReleasedImage( stopRel );
                    playButton.setCurrentImage( playPlaying );
                    playButton.setReleasedImage( playPlaying );
                }				
            }
            catch( QTException exc )
            {
                exc.printStackTrace();
            }
        }
    }
 
       ...
}


In the first line of the body of the actionPerformed( )method, we get the source of the event and store it in a temporary variable called source. This will allow us to determine which of the buttons the event is coming from.

Next, we check to see if the source of the event is the playButton. If it is, we know that the user clicked on the play button in the controller. When the user clicks the play button, there are two possibilities. The first is that the movie is already playing. In this case, we don't need to do anything to the movie because it is the correct state. All we need to do is make sure that the button images are correct. The second possibility is that the movie is stopped. If this is the case, we need to start the movie and then update the button states.

In order to do this correctly, we declare a conditional what checks to see if the rate of the md variable (the MoviePlayer) is 0. If the rate of the player is zero, the movie is stopped, and we need to start it by setting the rate to 1. If it is already playing, we can skip this step.

Next, we make sure that the buttons are using the correct images. This means that we need to make the stop button no longer active, and activate the play button. To do so, we call our setCurrentImage( ) method with the image of the button in the non-active state. We also call setReleasedImage( ), a method of the ReleaseButton class that sets the image that will be displayed when the button is released, with this same image so that the button will be restored correctly when it is pressed.

Similarly, we change the current and release image of the play button to the image of the button with the active blue highlight indicator.

Next, we will add code to handle the stop and rewind buttons:

AnimalPane.java
    ...

    public class ButtonListener implements QTActionListener	
    {
        public void actionPerformed( QTActionEvent event ) 
        {
            try
            {
                Object source = event.getSource();
				
                if( source.equals( playButton ))  
                {
                    if( md.getRate() == 0 )
                    {
                        md.setRate( 1 );
                    }
                    stopButton.setCurrentImage( stopRel );
                    stopButton.setReleasedImage( stopRel );
                    playButton.setCurrentImage( playPlaying );
                    playButton.setReleasedImage( playPlaying );
                }				
                else if( source.equals(stopButton))    
                {
                    if( md.getRate() == 1 )
                    {
                        md.setRate( 0 );
                    }
                    stopButton.setCurrentImage( stopDeactive );
                    stopButton.setReleasedImage( stopDeactive );
                    playButton.setCurrentImage( playRel );
                    playButton.setReleasedImage( playRel );
                }
                else if( source.equals(rewindButton ))
                {
                    md.setTime(0);
                }
            }
            catch( QTException exc )
            {
                exc.printStackTrace();
            }
        }
    }
 
       ...
}

After checking to make sure that the source of the action event is the stop button, we check to see if the movie is playing. If it is, we stop the movie by setting the rate of the movie presenter to 0. Next we update the button images using the same methodology as we used for the play button.

Finally, if the source was neither the play button, nor the stop button, it is the rewind button. We respond to the button press by rewinding the movie. In order to do this, we call setTime( ) from the movie presenter object with the parameter 0.

Now that we have a method that handles QTAction events, we need to create our controller and register the action listener with each of the buttons.

Back to top

Registering the Action Listeners

In order to make the buttons respond to user interaction, we need to register an action listener with each button:

AnimalPane.java
    ...

public class AnimalPane extends ZooPane
{
    ...
protected void addMovie( String moviePath, int layer, int x, int y ) {
        try
        {

            Movie m = makeMovie( new QTFile( QTFactory.
                findAbsolutePath( moviePath )));
            md = new MoviePresenter( m );
            md.setLocation(x, y);

            playRel     = makePresenterFromFile( 
                             "data/Play.jpg" );
            playPress   = makePresenterFromFile( 
                             "data/PlayPressed.jpg" );
            playPlaying = makePresenterFromFile( 
                             playPlaying );
            playButton.setLabel("Play");
            playButton.setLocation( x + 68, y + 152 );

stopRel = makePresenterFromFile( "data/Stop.jpg" ); stopPress = makePresenterFromFile( "data/StopPressed.jpg" ); stopDeactive= makePresenterFromFile( "data/Stopped.jpg"); stopButton = new MovieButton( stopRel, stopPress, stopDeactive ); stopButton.setLabel("Stop"); stopButton.setLocation( x + 136, y + 152 ); rewRel = makePresenterFromFile( "data/Rewind.jpg" ); rewPress = makePresenterFromFile( "data/RewindPressed.jpg" ); rewDeactive = rewRel; rewindButton = new MovieButton( rewRel, rewPress, rewDeactive ); rewindButton.setLabel( "Rewind" ); rewindButton.setLocation( x, y + 152 );
            ButtonListener bl = new ButtonListener();
			
            playButton.addActionListener(bl);
            stopButton.addActionListener(bl);
            rewindButton.addActionListener(bl);
 
compositor.addMember( md, 1ayer ); compositor.addMember(playButton); compositor.addMember(stopButton); compositor.addMember(rewindButton); compositor.getTimer().setRate(1); md.setRate(1); }
        catch( QTException exc )
        {
            exc.printStackTrace();
        }
        catch( IOException exc )
        {
            exc.printStackTrace();
        }	
    } 
       ...

While we are registering our listeners, it is a good time to add exception handling to our code. Several of the methods we used previously could throw exceptions, so it is a good time to put our code in a try / catch block.

The actual action listener registration is very simple. First, we create a new instance of our ButtonListener class we created in the previous step and assign it to a temporary variable. Next, we register our ButtonListener object with each of our buttons by calling the addActionListener( ) method.

Once we register our listener, when the user clicks on a button and releases it, the button sends its QTActionEvent to each QTActionListener that is registered with that button. This in turn will call the actionPerformed method of our ButtonListener which then checks the source of the action event and responds appropriately.

Finally, we catch any specific exceptions that may be thrown. At the risk of sounding like a broken record, it is a good time to mention that your code should preform better error handling.

So far, we have buttons that broadcast action events and a ButtonListener registered with each of these buttons that responds to the QTActionEvents. But this isn't quite enough. We need to let QuickTime know that our collection of buttons is a controller.

Back to top

Creating a Controller Object

In order for our buttons to work properly, we need to register a controller with QuickTime. To do this, we will use a QTMouseTargetController. This class allows us to specify an area or target that will receive events from QTJ in response to user interaction. These events occur on (but are not limited to) MouseEnter, MouseExit, MouseMove, and MouseDrag events.

This controller can act as a container and have members added to it which will also receive notification of mouseEvents. This is convenient for us because we have a number of buttons that we can add to the controller and have them automatically respond appropriately. We used the QTButton subclasses because they had almost all the functionality that we needed, but it is not a requirement to use these objects.

AnimalPane.java
    ...

public class AnimalPane extends ZooPane
{
    ...
protected void addMovie( String moviePath, int layer, int x, int y ) { ... ButtonListener bl = new ButtonListener(); playButton.addActionListener(bl); stopButton.addActionListener(bl); rewindButton.addActionListener(bl); compositor.addMember( md, 1ayer ); compositor.addMember(playButton); compositor.addMember(stopButton); compositor.addMember(rewindButton);
            buttonController = new 
               QTMouseTargetController(false);
            buttonController.addMember(playButton);
            buttonController.addMember(stopButton);
            buttonController.addMember(rewindButton);

            compositor.addController(buttonController);

            compositor.getTimer().setRate(1);
            md.setRate(1);
        } 

       ...

    protected ImagePresenter rewRel;
    protected ImagePresenter rewPress;
    protected ImagePresenter rewDeactive;
    protected MovieButton rewindButton;
    protected QTMouseTargetController buttonController;
}

Let's skip down to the very bottom of our source file where we will declare a data member object, buttonController, which is a QTMouseTargetController.

Next we go back up to our addMovie( ) routine, just after the code we added for adding our buttons to the compositor. We create a new QTMouseTargetController instance and assign it to our buttonController data member, passing false as the constructor parameter. This specificies that we do not want to use wholespace mode. If we were to specify whole space, every single object in our compositor would act like it was a member of the controller. Our background image, and movie would be members of the controller and inherit the same behavior. This is useful if you want a general behavior that pertained to every object in our compositor, such as the ability to be dragged around.

We don't want this behavior, however, so we specify false. This requires us to register objects with the controller that we want the controller to be associated with. Thus, the next three lines of code we add do exactly that. We call the addMember( ) method from the controller with each button object.

Finally, we add the controller to the compositor using the addController( ) method. This associates the controller with the compositor and ensures that all mouse events that occur within the compositor will be passed on to the controller.

So far, we have created three buttons, a MouseListener, and registered the listener with each of the buttons. Then, we created a QTMouseTargetController, added our buttons to it, and then added the controller to the compositor. But we aren't quite done yet. There's one critical step that we've missed. We have to register each of the buttons with the controller so that they can receive events from the controller.

Back to top

Registering the Buttons with the Controller

You may be thinking that things are really starting to get confusing. The following animation illustrates how all this works:

The animation above is slightly over-simplified in order to get the point across. The most important thing to understand is that all user interaction is first handled by the QTCanvas which then passes the message off to any registered controllers which in our case is the QTMouseTarget controller. In order to get the message from the controller to our button class, we need to use a ButtonActivator class:

AnimalPane.java
    ...

public class AnimalPane extends ZooPane
{
    ...
protected void addMovie( String moviePath, int layer, int x, int y ) { ... buttonController = new QTMouseTargetController(false); buttonController.addMember(playButton); buttonController.addMember(stopButton); buttonController.addMember(rewindButton); compositor.addController(buttonController);
            buttonActivator = new ButtonActivator();
            compositor.getTimer().setRate(1);
            md.setRate(1);
        } 

       ...

    protected ImagePresenter rewRel;
    protected ImagePresenter rewPress;
    protected ImagePresenter rewDeactive;
    protected MovieButton rewindButton;
    
    protected QTMouseTargetController buttonController;
    protected ButtonActivator buttonActivator;
}

 

The ButtonActivator class is an intermediary between the QTMouseTargetController and our button class. It processes the events in the target controller and sends them to the button class which we are listening to with our ButtonListener object.

In the source above, we add a protected ButtonActivator data member, and in the code body, create a new instance of that class. Let's now go look at where this ButtonActivator object is used.

Back to top

Implementing the Start Method

You may recall that we created a new class called ZooPane and derrived AnimalPane from that class. ZooPane had a start( ) and a stop( ) method that we must override in AnimalPane. These methods will tie our buttons into the event notification mechanism of the controller. Let's look first at start( ):

AnimalPane.java
    ...

public class AnimalPane extends ZooPane
{
    ...
public ImagePresenter makePresenterFromFile( String name ) { try { QTFile file = new QTFile( QTFactory.findAbsolutePath(name)); GraphicsImporterDrawer drawer = new GraphicsImporterDrawer(file); return ImagePresenter. fromGraphicsImporterDrawer(drawer); } catch(IOException e) { e.printStackTrace(); } catch(QTException e) { e.printStackTrace(); } return null; }
    public void start()
    {
        buttonController.addQTMouseListener(buttonActivator);
        try
        {
            stopButton.setCurrentImage(  stopRel );	
            playButton.setCurrentImage(  playPlaying ); 
            playButton.setReleasedImage( playPlaying );
        }
        catch( QTException e )
        {
            e.printStackTrace();
        }
        try
        {
            md.setRate(1);							
        }
        catch( QTException e )
        {
            e.printStackTrace();
        }
        compositor.getTimer().setRate(1);
    }
	
    public void stop()
    {
    }
    ... 
 
    protected void addMovie( String moviePath, int layer, 
        int x, int y )
    {
                ...
           
            buttonController = new 
               QTMouseTargetController(false);
            buttonController.addMember(playButton);
            buttonController.addMember(stopButton);
            buttonController.addMember(rewindButton);

            compositor.addController(buttonController);
            buttonActivator = new ButtonActivator();
            compositor.getTimer().setRate(1);
            md.setRate(1); 
        }            

    ... 

The start method is responsible for a number of things. Primarily, it adds the buttonActivator object to the buttonController as a registered mouse listener. Secondly, it makes sure that the controller is in the correct initial state and that the movie is playing,.

After declaring our start( ) method, we register the button activator with the controller by calling addQTMouseListener( ). Then, within a try / catch block, we set up the initial button images so that the stop button is not active, and play button is active.

In a second try block, we set the rate of the moviePresenter to one so that it starts playing immediately. We also move the compositor.getTimer( ).setRate( 1 ) routine from addMovie to the end of the start( ) function.

While we are at it, we declare our stop method which we will implement in the next step.

Back to top

Implementing the Stop Method

The stop method is designed to be called after we are finished with the movie controller and want to disassociate the buttons with the controller.

AnimalPane.java
    ...

public class AnimalPane extends ZooPane
{
    ...
public void start() { buttonController.addQTMouseListener(buttonActivator); try { stopButton.setCurrentImage( stopRel ); playButton.setCurrentImage( playPlaying ); playButton.setReleasedImage( playPlaying ); } catch( QTException e ) { e.printStackTrace(); } try { md.setRate(1); } catch( QTException e ) { e.printStackTrace(); } compositor.getTimer().setRate(1);
    
	
    public void stop()
    {
        buttonController.removeQTMouseListener(
            buttonActivator);
        compositor.getTimer().setRate(0);
        try
        {
            md.setRate(0);
        }
        catch( QTException e )
        {
            e.printStackTrace();
        }
    }

    ... 

The stop method is very similar to the start method, except that we are not concerned with making sure that the button is in the correct visible state. Since this method is designed to be used for cleaning things (for example when we have been removed from the compositor), we only care about stopping the movie from playing and unregistering our listeners.

We use the removeQTMouseListener( ) routine to unregister the button activator object with the controller. Then we change the rate of the compositor to stop it. Finally, we have a try / catch block that we need for stopping playback of the movie presenter.

This completes the source modifications for this file. We did a lot of work to get a custom controller, but the results will be worth it. We created a subclass of ReleaseButton and then a QTMouseListener subclass that we registered with each of our buttons. Then we created a QTMouseTargetController, added it to the compositor, and added our buttons to it. We then created a ButtonActivator object and registered that with the controller in the start( ) method. Finally, we added a stop( ) method that we can call when we do cleanup.

Now we have a little more modifications to do in Zoo5.java before we are finished.

Back to top

Final Modifications to the Zoo Class

Zoo5.java
    ... 
public class Zoo5 extends Frame
{
    static public int WIDTH  = 640;
    static public int HEIGHT = 480;	
	
    public Zoo5( String s ) 
    {
        super(s);
        setResizable( false ); 
        setBounds( 0, 0, WIDTH, HEIGHT );
		
        QTCanvas myQTCanvas = new QTCanvas( 
            QTCanvas.kInitialSize, 0.5F, 0.5F );
        add( myQTCanvas );
		
        AnimalPane zebraPane = new AnimalPane();      
        try 
        {
            myQTCanvas.setClient( zebraPane.getCompositor(), 
                true );
            zebraPane.start();
        }
        catch ( QTException e )
        {
            e.printStackTrace();
        }
		
        addWindowListener( new WindowAdapter()
        {
            public void windowClosing( WindowEvent we )
            {
                zebraPane.stop();
                QTSession.close();          
                dispose();                
            }
            public void windowClosed( WindowEvent we )
            {
                System.exit( 0 );            
            }
        });
    }

    ... 

In our main Zoo file, we add calls to the start( ) and stop( ) methods in our AnimalPane. We add the start method right after we create the AnimalPane object and set the client of the canvas. The stop( ) routine call is added in the windowClosing( ) method and will be called when the user clicks the close box of the window.

This completes the source code modifications to module 5. It's time to run your application and see the results.

If you would like to see the entire source file of either Zoo5.java, ZooPane.java, AnimalPane.java, or MovieButton.java, please follow the appropriate links.

Back to top

Summary

This module demonstated how to create and use custom QuickTime controllers for the presentation of an alternate user interface for time-based media interaction. We learned how to use QuickTime for Java's built-in button classes to create user interface elements that mimic traditional push-button controls. We extended the functionality of the button class to provide multiple state support, and we learned how to events are propogated from the compositor via the event listener mechanism.

Back to top

Further Exploration

The clever reader will realize quickly that the information that was presented here only scratches the surface of what could be done using QuickTime for Java. Although we will expand this example in future modules, there are few things that you could do on your own as exploration that will help you better understand how QuickTime for Java works.

Each one of these examples has a source code solution, so if you have trouble, feel free to consult it.

Experiment with one or more of the following:

  1. Modify the controller code so that clicking on any object in our compositor produces a beep.
  2. Modify the controller code so that the background image, movie, and buttons can be dragged around within the compositor.
  3. Change the behavior of the rewind button so that it plays the movie backwards at triple speed as long as the button is depressed.
  4. Add a button for fast-forwarding the movie
  5. Add a button that allows you to toggle between the standard controller and the custom controller.


Previous Section Table of Contents Next Section