OS Shell In Java
Volume Number: 15
Issue Number: 1
Column Tag: JavaTech
Writing an OS Shell in Java
by Andrew Downs
Building a Facade
Introduction
This article describes the design and implementation of a Finder-like shell written in
Java. Several custom classes (and their methods) areexamined: these classes provide a
subset of the Macintosh Finder's functionality. The example code presents some
low-level Java programming techniques, including tracking mouse events, displaying
images, and menu and folder display and handling. It also discusses several high-level
issues, such as serialization and JAR files.
Java provides platform-dependent GUI functionality in the Abstract Windowing Tookit
(AWT) package, and platform-independent look and feel in the new Java Foundation
Classes (Swing) API. In order to faithfully model the appearance and behavior of one
specific platform on another, it is necessary to use a combination of existing and
custom Java classes. Certain classes can inherit from the Java API classes, while
overriding their appearance and functionality. This article presents techniques used to
create a Java application (Facade) that provides some of the Macintosh Finder's
capabilities and appearance. As a Java application, it can theoretically run on any
platform supporting the Java 1.1 and JFC APIs. Figure 1 provides an overview of the
Facade desktop and its functionality. Figure 2 shows a portion of the underlying
object model. It contains a mixture of existing Java classes (such as Component and
Window) and numerous custom classes. In the diagram, the user interface classes are
separated from the support classes for clarity.
Figure 1. Facade on Windows 95.
Figure 2. Facade object model (partial). Only the inheritance structure is shown;
relational attributes are not shown.
The Object Model
Facade's object model utilizes the core Java packages and classes where possible.
However, in order to provide the appropriate appearance and speed, custom versions
of several standard Java classes were added. For instance, the DesktopFrame class is
used to display folder contents. Its functionality is similar to the java.awt.Frame class.
However, additional behavior (i.e. window-shade) and appearance requirements
dictated the creation of a "knockoff" class.
One advantage of using custom classes can be improved speed. This holds true for
classes inheriting from java.awt.Component: such classes do not require the creation of
platform-specific "peer" objects at runtime. Less baggage results in a faster response.
For example, the Facade menu display and handling classes respond very quickly.
In the custom classes, most drawing uses primitive functions to draw text, fill
rectangles, etc. Layout and display customization is assisted through the use of global
constant values which typically signify offsets from a particular location (as opposed
to hardcoded coordinates).
Several of the classes depicted in the object model will be discussed in this article.
Appearance and Functionality
Facade relies on several graphical user interface classes in order to provide a
Finder-like interface. These classes fall into the following categories:
• GUI
• Desktop
• Menus
• Icons
• Folders
• Support
• Startup
• Timing
• Constants
Facade provides a subset of the following operations:
• Menu and menu item selection
• Displaying volume/folder contents
• Dragging icons
• Opening and closing windows
• Managing the trash
• Saving the desktop state at shutdown (exit)
• Restoring the desktop state at startup
• Launching applications
• Dialog display
• Cut/Copy/Paste
• Changing the cursor
• Emulating the desktop database
Several of these operations (and the classes which implement them) will be discussed
in the following sections.
The Desktop
The Desktop class is a direct descendant of the java.awt.Window class, which provides a
container with no predefined border (unlike the java.awt.Frame class). This approach
allows the drawing and positioning of elements within the Desktop area, without
needing to hide the platform-specific scrollbars, title bar, etc.
However, this approach means that special care and feeding is required to make the
menubar work properly. In the current implementation, menubar (and menu)
drawing is done directly in the Desktop class, using values obtained from those
respective classes. In other words, menus don't draw themselves, Desktop does it for
them.
Menu Creation
The following code snippet (Listing 1), taken from the Desktop constructor, creates
the Apple menu, and populates it with one item. The menu is an instance of the
DesktopImage class. The Apple icon (imgApple) was loaded further up in this method. A
similar sequence creates and populates the File menu.
Listing 1: Desktop constructor
Desktop constructor
Creating menus in the Desktop constructor.
// ----------------------------------
// * Desktop constructor
// ----------------------------------
Desktop() {
//
// Create the menus and their menu items.
DesktopImageMenu dim;
Vector vectorMenuItems;
DesktopMenuItem dmi;
// Calculate the y-coordinate of the menus (constant).
int newY = this.getY() + this.getMenuBarHeight() -
( this.getMenuBarHeight() / 3 );
// Calculate the x-coordinate of the menus (variable).
int newX = this.getX();
// Create the Apple menu.
dim = new DesktopImageMenu();
dim.setImage( imgApple.getImage() );
dim.setX( imgApple.getX() );
dim.setY( imgApple.getY() );
// Create menu item "About...
dmi = new DesktopMenuItem( Global.menuItemAboutMac );
dmi.setEnabled( true );
// Add the menu item to the Vector for this particular menu...
dim.getVector().addElement( dmi );
// ...then tell the menu to calculate the item position(s).
// Note that the Desktop's FontMetrics object gets used here.
dim.setItemLocations( this.getGraphics().getFontMetrics() );
// Add the Apple menu to the menubar's Vector.
// The menubar was instantiated further up in this method.
dmb.getVector().addElement( dim );
// For the next menu, calculate its x-coordinate.
newX = imgApple.getX();
newX += ( 2 * Global.defaultMenuHSep );
newX += ( ( DesktopImageMenu )dmb.getVector().elementAt( 0 )
).getImage().getWidth( this ); // Line wrap.
DesktopMenu dm;
// Create the File menu...
dm = new DesktopMenu();
dm.setLabel( Global.menuFile );
dm.setX( newX );
dm.setY( newY );
// ...and all its menu items.
dmi = new DesktopMenuItem( Global.menuItemNew );
dmi.setEnabled( true );
dm.getVector().addElement( dmi );
dmi = new DesktopMenuItem( Global.menuItemOpen );
dmi.setEnabled( false );
dm.getVector().addElement( dmi );
dmi = new DesktopMenuItem( Global.menuItemClose );
dmi.setEnabled( false );
dm.getVector().addElement( dmi );
// Tell the menu to calculate the item position(s).
dm.setItemLocations( this.getGraphics().getFontMetrics() );
// Add the File menu to the menubar's Vector.
dmb.getVector().addElement( dm );
newX += ( 2 * Global.defaultMenuHSep );
newX += theFontMetrics.stringWidth( dm.getLabel() );
//
}
Here, the variable dim is an instance of the class DesktopImageMenu. This specialized
menu class simply adds an instance variable that contains an Image (in this case, the
Apple) to the DesktopMenu class. Its behavior is the same as other menus, except that
instead of a String it displays its Image. The x and y coordinates of this menu are
determined by the same values assigned to the image when it was loaded.
This object contains one menu item, the "About" item. That item is enabled, and added
to the Vector for the menu. This Vector contains all the menu items for that menu. This
approach provides an easy way to manage and iterate over the menu items.
Once the Vector has been setup, its contents are given their x-y coordinates for
drawing and selection purposes through the setItemLocations() method. Although this
calculation can be done when the menu is selected, the code runs faster if those
numbers are calculated and assigned at startup time.
Finally, the menu is added to the Vector for the menubar. The menubar uses a Vector to
iterate over its menus, in the same way the menus iterate over their menu items. This
will become apparent when examining the mouse-event handling code for the Desktop
class.
The same approach is shown for the File menu, except that File contains a String
instead of an Image, as well as several menu items.
Menu and Menu Item Selection
This class defines some global values that are shared between the Display and
ModeSelector classes. These values are collected in one place to simplify housekeeping
and maintenance. We will refer to them from the other classes as Global.NORMAL,
Global.sleep, etc. This is the Java syntax for referencing static (class) attributes. Note
that final means the values cannot be changed after they are initially assigned, so these
are constants.
Figure 3. Menu item selection.
The code in Listing 2 handles mouseDown events in the menubar. The first two lines
reset the instance variables used to track the currently active menu and menu item.
Since the user has pressed the mouse, the old values do not apply. Next, the Graphics
and corresponding FontMetrics objects for the Desktop instance are retrieved. They
will be used in drawing and determining what selection the user has made.
Next, test to see whether the event occured within the bounding rectangle of the
menubar. This line uses the java.awt.Component.contains() method. If the x-y
coordinates of the mouse event are inside the menubar, then determine which menu (if
any) the user selected.
Determining the menu selection uses the menubar's Vector of menus. Iterate over the
Vector elements, casting each retrieved element to a DesktopMenu object.
(Vector.elementAt() returns Object instances by default, which won't work here.) The
menu has its own bounding rectangle, which is compared to the event x-y coordinates.
In addition, in order to duplicate the Finder, a fixed pixel amount is subtracted from
the left-side of the bounding rectangle, and added to the right-side. This allows the
user to select near, but not necessarily directly on, the menu name (or image). Notice
also in the (big and nasty) if conditional that the DesktopMenu retrieved from the
Vector is checked for an Image (this handles the Apple menu). If it has one, the image
width is used instead of the String width.
Once the user's menu selection has been found, it is redrawn with a blue background.
Then, the menu items belonging to that menu are drawn inside a bounding rectangle
(black text on white background). The menu item coordinates, and the corresponding
bounding rectangle coordinates, were calculated when the menu was created, saving
some CPU cycles here.
Listing 2: mousePressed
mousePressed
The mousePressed method handles menu selections.
// ----------------------------------
// * mousePressed
// ----------------------------------
public void mousePressed( MouseEvent e ) {
// New click means no previous selection.
this.activeMenu = -1;
this.activeMenuItem = -1;
Graphics g = this.getGraphics();
FontMetrics theFontMetrics = g.getFontMetrics();
if ( dmb.contains( e.getX(), e.getY() ) ) {
// Handle menu selection.
for ( int i = 0; i < dmb.getVector().size(); i++ ) {
// Get menu object.
DesktopMenu d = ( DesktopMenu )dmb.getVector().elementAt( i );
// Determine if we're inside this menu.
// This could be done with one or more Rectangles.
// Note the (buried) conditional operator: it accounts
// for both text and image (e.g. the Apple) menus.
if ( ( e.getX() >= d.getX() - Global.defaultMenuHSep )
&& ( e.getX() <= ( d.getX() + ( d.getLabel() == null ?
( ( DesktopImageMenu )d ).getImage().getWidth( this ) :
theFontMetrics.stringWidth( d.getLabel() ) ) +
Global.defaultMenuHSep ) )
&& ( e.getY() >= this.getY() ) && ( e.getY() <=
this.getMenuBarHeight() ) ) {
// Draw menubar highlighting...
g.setColor( Color.blue );