An Example Swing Application

This section introduces a fairly comprehensive Swing application and details its various components. This application is used throughout the rest of this book as the foundation application (so you can run each sample without having to create new windows, set up the menu bar, and perform other mundane tasks). For that reason, be sure to code and compile the application and have the source code handy before diving into future chapters.

Tip

For those of you who don’t like large code blocks, especially when it’s mostly GUI setup code, understand that this section is a necessary evil; it’s the basis for several future applications, all of which are made up of more interesting code fragments and concepts.

Source Code Listing

This application also demonstrates techniques that factor out and partition portions of your application to increase team development, as well as an introduction to concepts (such as dynamic class loading) used in later chapters. In this example, these modules are referred to as plug-ins. If you’ve used an application such as Adobe Photoshop or the NetBeans development environment, you’ve been exposed to applications that serve as a framework and then load other modules. Example 4-1 shows the entire class listing.

Example 4-1. The sample Swing application

package com.wiverson.macosbook;

import javax.swing.JMenuItem;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JMenu;
import java.awt.Cursor;
import java.awt.BorderLayout;
import java.util.Hashtable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import javax.swing.KeyStroke;

public class SimpleEdit extends javax.swing.JFrame
{
    // Used to set the number for new untitled windows
    private static int newWindows = -1;

    // Used to track all of the currently installed plugins
    private static Hashtable plugins = null;
    
    // The initial plugin configuration
    private static String[] argsconfig;
    
    /** Creates a new instance of SimpleEdit */
    public SimpleEdit(  )
    {
        init(  );
    }

/* --------------------- Application APIs ------------------------------------ */

    /** Used by tools to get the text actual text area.
     * This wouldn't generally be recommended, but in this
     * case it's ok.
     *
     * In general, you'd want to use something to make the
     * interface more opaque (thereby freeing up options to
     * switch to a different underlying toolkit), but in this
     * case it would cost readability (since everyone can look
     * up a JTextArea).
     */
    public javax.swing.JTextArea getJTextArea(  )
    {
        return this.mainTextArea;
    }
    
    /** Used by tools to get the current text */
    public String getDocumentText(  )
    {
        return this.mainTextArea.getText(  );
    }
    
    /** Used by tools to set the current text */
    public void setDocumentText(String in)
    {
        mainTextArea.setText(in);
        mainTextArea.setCaretPosition(mainTextArea.getText().length(  ));
        mainTextArea.requestFocus(  );
    }
    
    /** Used by tools to add to the current text */
    public void appendDocumentText(String in)
    {
        setDocumentText(mainTextArea.getText(  ) + in);
    }
    
    /** Used by tools to set the status text at the bottom
     * of a frame.
     */
    public void setStatusText(String in)
    {
        this.mainStatusText.setText(in);
    }

/* --------------------- Initialization ------------------------------------ */

    // Sets up and creates a "pristine" window environment
    private void init(  )
    {
        if(newWindows++ < 0)
            setTitle("Untitled");
        else
            setTitle("Untitled-" + newWindows);
                
        initPlugins(  );
        initComponents(  );
        initMenuBar(  );
    }

/* --------------------- Initialization: Plugins  ---------------------------- */
    
    // Installs all plugins as currently defined by the
    // private argsconfig.
    private void initPlugins(  )
    {
        if(plugins != null)
            return;
        if(argsconfig == null)
            return;
        if(argsconfig.length == 0)
            return;
        plugins = new Hashtable(  );
        
        for(int i = 0; i < argsconfig.length; i++)
        {
            // This may very well fail, as we are going
            // to be loading classes by name, which is
            // prone to errors (e.g. typos, etc.)
            try
            {
                // This requests the classloader to find a
                // given class by name.  We are using this to
                // implement a plugin architecture, based on
                // expecting classes to implement a specific
                // interface (SimpleEditPlugin).  If the class
                // can be loaded and cast without failure,
                // we are good to go.
                Class myClass = Class.forName(argsconfig[i]);
                SimpleEditPlugin myPlugin = 
                 (SimpleEditPlugin)myClass.getConstructor(null).newInstance(null);
                
                // Don't add the plugin if already installed. Allows for 
                // eventual support for dynamically adding new plugins later.  
                // Calls the Plugin init if this is the first time 
                // it's being loaded.
                if(plugins.containsKey(myPlugin.getAction(  )))
                {
                    return;
                } else
                {
                    myPlugin.init(this);
                }
                
                // If we made it this far, the plugin has been loaded
                // and initialized, so it's ok to add to the list of
                // valid plugins.
                plugins.put(myPlugin.getAction(  ), myPlugin);
            }
            catch(Exception e)
            {
                // This is not really adequate for a quality client 
                // application, but it's acceptable for our purposes.
                System.out.println("Couldn't load Plugin: " + argsconfig[i]);
                System.out.println(e.getMessage(  ));
                e.printStackTrace(  );
            }
        }
    }

/* --------------------- Initialization: GUI Components---------------------- */

    // The main visual components
    private javax.swing.JScrollPane mainScrollPane = new javax.swing.JScrollPane(  );
    private javax.swing.JTextArea mainTextArea = new javax.swing.JTextArea(  );
    private javax.swing.JToolBar mainToolBar = new javax.swing.JToolBar(  );
    private javax.swing.JTextField mainStatusText= new javax.swing.JTextField(  );
    
    private void initComponents(  )
    {
        this.getContentPane(  ).setBackground(java.awt.Color.white);
        this.getContentPane().setLayout(new BorderLayout(  ));
        this.getContentPane(  ).add(mainScrollPane, BorderLayout.CENTER);
        this.getContentPane(  ).add(mainToolBar, BorderLayout.NORTH);
        this.getContentPane(  ).add(mainStatusText, BorderLayout.SOUTH);
        
        // This text field serves two purposes. It provides useful information
        // to the user, and also serves as a graceful "bump" for the Mac OS
        // grow box on the Mac OS platform.
        mainStatusText.setText("Ready.");
           
        mainStatusText.setCursor(
             Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
        
        mainScrollPane.setViewportView(mainTextArea);
        
        mainTextArea.setEditable(true);
        mainTextArea.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
        mainTextArea.setFont(
            new java.awt.Font("serif", java.awt.Font.PLAIN, 12)
            );

        // Perhaps a tool might be added later to control this dynamically?
        mainTextArea.setLineWrap(true);
        
        // Generally looks terrible on all platforms, and requires
        // a fair amount of work to get it to work right.
        mainToolBar.setFloatable(false);
        initToolBar(mainToolBar, this);
        
        // Determine the offset value and stagger new windows
        // (with a reset every ten windows). A somewhat
        // unscientific mechanism, but it works well enough.
        int top_offset = 0;
        if((newWindows % 10) > 0)
        {
            top_offset =((this.newWindows) % 10) * 20 + 20;
            
            this.setLocation(
                 new Double(getLocation().getX() + top_offset - 20).intValue(  ),
                 new Double(getLocation().getY() + top_offset).intValue(  )
            );
        }
        int bottom_offset = 0;
        if (top_offset > 0)
            bottom_offset = top_offset - 20;
        
        // In a later chapter, we can use the JDirect and the
        // Carbon API GetAvailableWindowPositioningBounds(  )
        // to properly position this.
        java.awt.Dimension screensize =
             java.awt.Toolkit.getDefaultToolkit().getScreenSize(  );
        screensize = 
             new java.awt.Dimension(640, screensize.height -128 - bottom_offset);
        this.setSize(screensize);
    }

    // Default items that always appear on the toolbar.
    // null items are treated as separators.
    String[] toolbarItems = {"New", "Open", null, "Timestamp"};

    private void initToolBar(javax.swing.JToolBar myToolBar, SimpleEdit myFrame)
    {
        JButton newButton;
        for(int i = 0; i < toolbarItems.length; i++)
        {
            if(toolbarItems[i] != null)
            {
                // It would be nice to provide icons
                // instead of just text labels.
                newButton = new JButton(toolbarItems[i]);
                
                // Used to track the targets more easily
                newButton.putClientProperty("window", myFrame);
                newButton.addActionListener(actionListenerHandler);
                myToolBar.add(newButton);
            } else
            {
                myToolBar.add(new javax.swing.JToolBar.Separator(  ));
            }
        }
        
        // Load all plugins into the toolbar
        if(plugins != null)
            if(plugins.size(  ) > 0)
            {
                java.util.Enumeration e = plugins.elements(  );
                SimpleEditPlugin currentPlugin;
                while(e.hasMoreElements(  ))
                {
                    currentPlugin = (SimpleEditPlugin)e.nextElement(  );
                    newButton = new JButton(currentPlugin.getAction(  ));
                    // We are using Swing client properties to 
                    // track additional information without having
                    // to subclass - in effect, using the
                    // client properties mechanism as a form of
                    // delegation.
                    newButton.putClientProperty("window", myFrame);
                    newButton.putClientProperty("plugin", currentPlugin);
                    newButton.addActionListener(actionListenerHandler);
                    myToolBar.add(newButton);
                }
            }
    }

/* --------------------- Initialization: Menu Bar  ---------------------------- */

    // The menu bar for the window
    private javax.swing.JMenuBar mainMenuBar = new javax.swing.JMenuBar(  );
    
    // The menus attached to the menu bar
    private JMenu menuFile = new JMenu(  );
    private JMenu menuEdit = new JMenu(  );
    private JMenu menuTools = new JMenu(  );
    
    // A Hashtable holding all of the default menu items, keyed by title
    protected static Hashtable menuItemsHashtable = new Hashtable(  );
    
    /*
     * The items to be installed into the menus.
     * Each item consists of an identification string and
     * a corresponding virtual key.
     *
     * For a "real" application, the default item titles
     * and virtual keys would be loaded from resource bundles,
     * and ideally the user would be able to configure their
     * own toolbar and menu structure.
     *
     * For this demonstration, however, this is adequate.
     */
    private Object[][] fileItems =
    {
        {"New", new Integer(KeyEvent.VK_N)},
        {"Open", new Integer(KeyEvent.VK_O)},
        {"Close", new Integer(KeyEvent.VK_W)},
        {null, null},
        {"Save", new Integer(KeyEvent.VK_S)},
        {"Revert to Saved", null},
        {null, null},
        {"Print...", new Integer(KeyEvent.VK_P)},
        {"Print Setup...", null}
    };
    private Object[][] editItems =
    {
        {"Undo", new Integer(KeyEvent.VK_Z)},
        {"Redo", new Integer(KeyEvent.VK_Y)},
        {null, null},
        {"Cut", new Integer(KeyEvent.VK_X)},
        {"Copy", new Integer(KeyEvent.VK_C)},
        {"Paste", new Integer(KeyEvent.VK_V)},
        {"Delete", null},
        {"Select All", new Integer(KeyEvent.VK_A)}
    };
    private Object[][] toolItems =
    {
        {"Timestamp", null}
    };

    private void dispatchEvent(ActionEvent evt, String tag)
    {
        SimpleEdit myFrame = null;
        SimpleEditPlugin myPlugin = null;
        if(evt.getSource(  ) instanceof JComponent)
        {
            myFrame = (SimpleEdit)
                        (((JComponent)evt.getSource(  )).getClientProperty("window"));
            myPlugin =  (SimpleEditPlugin)
                        (((JComponent)evt.getSource(  )).getClientProperty("plugin"));
        }
        
        // If it's a plugin, hand off to the plugin to handle
        if(myPlugin != null)
        {
            myPlugin.doAction(myFrame, evt);
            return;
        }
        
        // Handle minimal required functionality.
        // It could legitimately be argued that even this
        // functionality should be split off into an
        // overarching set of plugin functionality...
        // but this is adequate for now, and reinforces
        // the notion of certain "default" services.
        if(tag.compareTo("New") == 0)
            doNew(  );
        if(tag.compareTo("Close") == 0)
            doClose(myFrame);
        if(tag.compareTo("Timestamp") == 0)
            doTimestamp(myFrame);
    }

    /*
     * Default event processing.
     */
    private void doNew(  )
    {
        (new SimpleEdit()).show(  );
    }
    
    private void doTimestamp(SimpleEdit myFrame)
    {
        if(myFrame != null)
           myFrame.mainTextArea.setText(myFrame.mainTextArea.getText(  ) +
                 System.getProperty("line.separator")  + new java.util.Date(  ) + " : ");
        
             myFrame.mainTextArea.setCaretPosition(
              myFrame.mainTextArea.getText().length(  ));
        myFrame.mainTextArea.requestFocus(  );
    }

    // Used to track the number of open windows, and
    // automatically quit when they are all closed.
    private static int openWindows = 0;
    
    // Overrides the default hide to see how many windows are currently
    // showing. If none are visible, quit the app.
    /** Hides the window. If no windows are visible, terminates quietly. */
    public void hide(  )
    {
        super.hide(  );
        openWindows--;
        if(openWindows == 0)
            System.exit(0);
    }
    
    public void show(  )
    {
        super.show(  );
        openWindows++;
        // All ready to go, go ahead and get ready for input.
        this.appendDocumentText("");
    }
    
    private void doClose(SimpleEdit myFrame)
    {
        myFrame.hide(  );
    }

    /* This variable is used to track the default accelerator 
     * key for this platform.
     */
    private int preferredMetaKey =
        Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(  );

    private void setupMenu(JMenu myMenu, Object[][] menuconfig, 
        SimpleEdit thisFrame)
    {
        JMenuItem currentMenuItem;
        for(int i = 0; i < menuconfig.length; i++)
        {
            if(menuconfig[i][0] != null)
            {
                currentMenuItem = new JMenuItem(  );
                currentMenuItem.setLabel((String)menuconfig[i][0]);
                
                if(menuconfig[i][1] != null)
                {
                    int keyCode = ((Integer)menuconfig[i][1]).intValue(  );
                    KeyStroke key = 
                 KeyStroke.getKeyStroke(keyCode, preferredMetaKey);
                    currentMenuItem.setAccelerator(key);
                }
                
                currentMenuItem.setEnabled(false);
                currentMenuItem.setActionCommand((String)menuconfig[i][0]);
                currentMenuItem.putClientProperty("window", thisFrame);
                
                currentMenuItem.addActionListener(actionListenerHandler);
                
                // Put the menu item into the menu hash to add handlers later
                menuItemsHashtable.put((String)menuconfig[i][0], currentMenuItem);
                myMenu.add(currentMenuItem);
            } else
            {
                javax.swing.JSeparator sep = new javax.swing.JSeparator(  );
                myMenu.add(sep);
            }
        }
    }

    // A single default ActionListener that punts to dispatchEvent(  ).
    private ActionListener actionListenerHandler = new ActionListener(  )
    {
        public void actionPerformed(ActionEvent evt)
        {
            Object src = evt.getSource(  );
            if(src instanceof JMenuItem)
            {
                String input = ((JMenuItem)src).getLabel(  );
                dispatchEvent(evt, input);
            }
            if(src instanceof JButton)
            {
                String input = ((JButton)src).getLabel(  );
                dispatchEvent(evt, input);
            }
        }
    };

    private void initMenuBar(  )
    {
        mainMenuBar = new javax.swing.JMenuBar(  );
        
        menuFile = new JMenu("File");
        setupMenu(menuFile, fileItems, this);
        mainMenuBar.add(menuFile);
        
        menuEdit = new JMenu("Edit");
        setupMenu(menuEdit, editItems, this);
        mainMenuBar.add(menuEdit);
        
        menuTools = new JMenu("Tools");
        setupMenu(menuTools, toolItems, this);
        mainMenuBar.add(menuTools);
        
        JMenuItem newMenuItem;
        if(plugins != null)
            if(plugins.size(  ) > 0)
            {
                java.util.Enumeration e = plugins.elements(  );
                SimpleEditPlugin currentPlugin;
                while(e.hasMoreElements(  ))
                {
                    currentPlugin = (SimpleEditPlugin)e.nextElement(  );
                    newMenuItem = new JMenuItem(  );
                    newMenuItem.setLabel(currentPlugin.getAction(  ));
                    newMenuItem.setEnabled(true);
                    newMenuItem.setActionCommand(currentPlugin.getAction(  ));
                    newMenuItem.putClientProperty("window", this);
                    newMenuItem.putClientProperty("plugin", currentPlugin);
                    newMenuItem.addActionListener(actionListenerHandler);
                    menuTools.add(newMenuItem);
                }
            }
        
        ((JMenuItem)menuItemsHashtable.get("New")).setEnabled(true);
        ((JMenuItem)menuItemsHashtable.get("Timestamp")).setEnabled(true);
        ((JMenuItem)menuItemsHashtable.get("Close")).setEnabled(true);
        
        setJMenuBar(mainMenuBar);
    }
/* ----------------- The Main Method: Menu Bar  ---------------------------- */

    public static void main(String[] args)
    {
        argsconfig = args;
        (new SimpleEdit()).show(  );
    }
}

If you’re new to Swing, you may wish to look up the reference material for imported classes. You’ll also see several other classes named explicitly in the code (like javax.swing.JScrollPane) rather than imported, chiefly for self-documentation (and also because Swing occasionally has duplicate names in multiple packages). You’ll also notice that the example class extends JFrame, a pretty standard technique in Swing, and calls an initialization routine in its constructor.

Using an init( ) method instead of working in the constructor allows you to recycle an object instead of allocating a new one:

// Create the object the first time
SimpleEdit editor = new SimpleEdit(  );

// Do some work that changes the state of the editor object

// This takes more time and memory, depending on the JVM
editor = new SimpleEdit(  );

// This approach is better and faster (instead of using new)
editor.init(  );

Using an init( ) method is not actually required in this implementation, but it’s a good pattern for instances in which you might reuse the object without creating a new one. For example, you could use the init( ) method to reset the window to a “pristine” state before loading a file from disk.

The Application API

After the constructor, you’ll see several methods that aid in text manipulation (such as getDocumentText( ) or getJTextArea( )). While these examples could be considered utility methods, they form the backbone of this application’s application programming interface (API). In other words, other applications could use this class as a module, with text editing capability, through these methods. The class provides a notebook-style framework that you could use in a code editor or journaling software, for instance. As a result, these utility methods become very important—applications using this class will depend on the methods for interaction with SimpleEdit.

Initialization

Moving back into the variable declaration section of the class, you’ll see a private counter that keeps track of new and untitled document names:

// Used to set the number for new untitled windows
private static int newWindows = -1;

This counter determines the offset at which to place the document window, as well as the window identifier (to help the user keep multiple untitled windows distinct). It is also critical for the init( ) method:

    // Sets up and creates a "pristine" window environment
    private void init(  )
    {
        if(newWindows++ < 0)
               setTitle("Untitled");
               else
               setTitle("Untitled-" + newWindows);
                
        initPlugins(  );
        initComponents(  );
        initMenuBar(  );
    }

Loading plug-ins

What happens next is interesting: the init( ) method calls initPlugins( ), which then loads any plug-ins that the class is instructed to bring online (more on that in a minute). Of course, I just told you that this class has an API that other applications can use for accessing it. This means that the sample class suddenly has dual uses: as a framework to be used by other applications (basically a plug-in), and as an application in its own right, able to load its own plug-ins. This is a common concept in GUI programming: components are both containers and units that are contained. The API methods become more critical as applications external to the class use them to operate on the framework, and plug-ins internal to it might need to call back to them.

In the initPlugins( ) method, you’ll see much of the heavy lifting performed as plug-ins are loaded. The plug-ins are specified by a set of command-line arguments that specify the class name of each desired plug-in. The JVM class loader then has to find the class by name.

Classes are loaded and then cached in a Hashtable (called, not surprisingly, plugins). This improves performance: the plug-ins are loaded only when the application is first launched. Traditionally, Mac OS users often expect delays when an application initially starts, but once they begin to work, lengthy delays are unacceptable.

You’ll note that the plug-in classes are cast to the type SimpleEditPlugin (the next section deals with this interface, so don’t get too impatient). Plug-ins are expected to implement this interface. Plug-in authors are given this interface and the application APIs, which are sufficient for writing additional modules that this framework can use.

GUI components

Next comes the initComponents( ) method, which creates the core interface of the application’s main window. It’s fairly straightforward. Check the Swing documentation for details on how the specific APIs are used.

This method then delegates additional GUI processing to initToolbar( ) , which does what it says it does: it deals with the application toolbar and its buttons. The application uses both the default application actions and the plug-in configuration options to create a user toolbar.

Menu bars

The menu bar configuration is handled via multiple-dimension arrays:

                  private Object[][] fileItems =
    {
        {"New", new Integer(KeyEvent.VK_N)},
        {"Open", new Integer(KeyEvent.VK_O)},
        {"Close", new Integer(KeyEvent.VK_W)},
        {null, null},
        {"Save", new Integer(KeyEvent.VK_S)},
        {"Revert to Saved", null},
        {null, null},
        {"Print...", new Integer(KeyEvent.VK_P)},
        {"Print Setup...", null}
    };
    private Object[][] editItems =
    {
        {"Undo", new Integer(KeyEvent.VK_Z)},
        {"Redo", new Integer(KeyEvent.VK_Y)},
        {null, null},
        {"Cut", new Integer(KeyEvent.VK_X)},
        {"Copy", new Integer(KeyEvent.VK_C)},
        {"Paste", new Integer(KeyEvent.VK_V)},
        {"Delete", null},
        {"Select All", new Integer(KeyEvent.VK_A)}
    };
    private Object[][] toolItems =
    {
        {"Timestamp", null}
    };

This technique isn’t very object-oriented, but it lays out the menu visually in the code, making it easy to see how the menu bar will look when rendered graphically.

Items defined as {null, null} are treated as separator bars. The action name is defined by the first element in each mini-array, and the second Integer element specifies the keyboard accelerator constant.

Tip

You can look up these key constants in any good Swing reference.

A lot of code is associated with getting these menu bars to actually do something useful, beginning with the dispatchEvent( ) method. The event handling is performed mainly by a single handler, with Swing client property values used to store and handle event dispatching. Basically, the framework handles the most basic actions (calling upon the doNew( ) , doTimeStamp( ), and doClose( ) methods), while everything else is handed off to the appropriate plug-in to handle on its own.

Once event handling is dealt with, add this trick to your toolkit:

    /* This variable is used to track the default accelerator 
     * key for this platform.
     */
    private int preferredMetaKey =
         Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(  );

This little-known API is critical for proper cross-platform user interface application development. Mac OS X users expect to use the Meta key, officially referred to as the Command key (although I still tend to call it the “open apple” key). Windows applications, however, typically use the Control key. This application allows both options to map to the same UI action.

Many Java applications are hardcoded to use the control character as their default keyboard accelerator. Ironically, a user (or developer) used to Windows would consider this situation an ordinary feature: the same application running on two different platforms uses the same keyboard commands. However, it’s terrible for any real Mac OS X user. Use this API when determining the default keyboard shortcuts and, if possible, include an interface that reassigns keyboard commands.

Finally, the menu can actually be created with the setupMenu( ) and initMenuBar( ) methods (the latter of which was called in the init( ) method). These methods use the arrays defined earlier to set up the menus, and the various event handlers to react to them. Some additional Swing client properties assist with event dispatching.

This menu bar is created and set for each window whenever a window is created. This serves an important purpose: on Windows and other windowing toolkits, each window is given a specific menu bar within the bounds of the window itself. On Mac OS X, a proper application has a single global menu bar shared by all windows. This application doesn’t care which model is selected, and functions identically on all platforms.

Event processing

You learned about event processing and the dispatchEvent( ) method earlier, but now let’s look at the doClose( ) method and its two helpers, hide( ) and show( ):

    public void hide(  )
    {
        super.hide(  );
        openWindows--;
        if(openWindows == 0)
            System.exit(0);
    }
    
    public void show(  )
    {
        super.show(  );
        openWindows++;
        // All ready to go, go ahead and get ready for input.
        this.appendDocumentText("");
    }
    
    private void doClose(SimpleEdit myFrame)
    {
        myFrame.hide(  );
    }

Note that the application terminates automatically when the final window is closed. This termination provides a relatively seamless user experience, minimizing the user’s awareness of the application as a process that runs independently of any documents.

Tip

To get an idea of how this scenario affects your system, open Calculator, Address Book, and Internet Explorer. Now close out all your windows; note that while Calculator and Address Book are no longer running, Internet Explorer still waits for a “Quit” command. The example application behaves like Calculator and Address Book, assuming that when all windows are closed, the application should follow suit.

Last but not least, at the end of the code lies a simple main( ) method:

public static void main(String[] args)
    {
        argsconfig = args;
        (new SimpleEdit()).show(  );
    }

With a solid set of methods already in hand, the main( ) method only has to configure the application and open a single window. It calls the constructor for the SimpleEdit class, which in turn calls the init( ) method, which then sets up the various plug-ins for the application.

The SimpleEditPlugin Interface

The SimpleEditPlugin interface allows the SimpleEdit application to deal with plug-ins at runtime, assuming that they implement this public interface. I’ve left the discussion of this interface for the end of this chapter so that you first learn how to use Swing with Mac OS X and don’t get too hung up in inheritance and polymorphism.

This application demonstrates how to use dynamic class loading to let the same system add additional functionality without changing the core application. Chapter 5 uses a similar technique to isolate Mac OS X-specific code from the rest of the application. In a nutshell, the SimpleEditPlugin interface abstracts plug-in-specific (or platform-specific) details from SimpleEdit, and lets SimpleEdit handle all plug-ins generically.

The code shown in the initPlugins( ) method (see Example 4-1) relies on the fact that each plug-in implements a specific interface so it can easily call methods on a loaded class, as shown here:

Class myClass = Class.forName(argsconfig[i]);
    SimpleEditPlugin myPlugin = 
        (SimpleEditPlugin)myClass.getConstructor(null).newInstance(null);

Example 4-2 shows the actual SimpleEditPlugin interface. As you can see, it is remarkably simple.

Example 4-2. The SimpleEditPlugin interface

package com.wiverson.macosbook;

public interface SimpleEditPlugin
{
    // Returns a list of actions which will be registered
    // The tool will then be notified if an action is
    // selected.
    public String getAction(  );
    
    // Notification of an action which was registered by
    // this tool.
    public void doAction(SimpleEdit frame, java.awt.event.ActionEvent evt);
    
    // Called once when the plugin is first loaded
    public void init(SimpleEdit frame);
    
}

Code that implements this interface is loaded by SimpleEdit when it is launched with one or more command-line parameters with the fully qualified class name. As long as the interface is followed, SimpleEdit can interact with the plug-in.

Writing a plug-in

An example of a simple plug-in is one that simply beeps when the user invokes it from the menu bar or toolbar. Example 4-3 shows how easily you can code up such a plug-in.

Example 4-3. A beeping plug-in

package com.wiverson.macosbook.plugin;

import com.wiverson.macosbook.SimpleEdit;

public class BeepPlugin implements com.wiverson.macosbook.SimpleEditPlugin
{
    
    public BeepPlugin(  )
    {
    }
    
    public void doAction(SimpleEdit frame, java.awt.event.ActionEvent evt)
    {
        java.awt.Toolkit.getDefaultToolkit().beep(  );
        frame.setStatusText("Beep!");
      
    }
    
    public String getAction(  )
    {
        return "Beep";
    }
    
    public void init(SimpleEdit frame)
    {
        frame.setStatusText("Loaded BeepPlugin");        
    }
    
}

Assuming that this plug-in was compiled successfully, it is possible to launch SimpleEdit with this installed plug-in by executing the following command:

java com.wiverson.macosbook.SimpleEdit com.wiverson.macosbook.plugin.
BeepPlugin

Once the SimpleEdit application starts up and detects a command-line argument, it loads the named plug-in. The getAction( ) method tells SimpleEdit which text to display in the user interface, and when the user selects this option, the doAction( ) method is called. The arguments passed in allow the plug-in to affect the window and text area by working with the passed-in SimpleEdit object. In this manner, the plug-in is isolated from all user interface details of SimpleEdit.

In later chapters we will use this plug-in architecture to add everything from spellcheck capabilities to communication with web services. The next chapter uses this mechanism to add support for Apple-specific extensions to the Java platform.

Get Mac OS X for Java Geeks now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.