CocoaWGet: implementing code with Project Builder

Một phần của tài liệu Programming mac OS x a guide for unix developers (Trang 247 - 260)

Now that you have the program’s interface built, it is time to look at some code. The CocoaWGet program is composed of five controller classes and some support classes.

The controller classes include one main application controller that drives the applica- tion and four subcontrollers that mediate messages between the main controller and the tabbed view panes. The support classes include a task class that is responsible for running the wgetUNIX command-line program, collecting the output of the program, and returning the output to the client; the application support class, which handles miscellaneous support tasks for the program; and the parameters class, which stores wget options. Figure 6.2 shows a simple class diagram of the program.

Figure 6.13

The main steps in designing a Cocoa program, from Interface Builder to Project Builder

Rather than stepping you through the stages of adding code application code to the program, I’ll instead detail each class, showing how it works and how it fits into the application. Refer to the code examples here as well as the full source code from the project.

6.6.1 The model

As you recall from the previous discussion of the MVC pattern, the model is responsible for maintaining the data state of the program and responding to client messages that query the state for values or update the state of the model.

WGetParameters class

The CocoaWGet program’s model is represented by the WGetParameters class (see figure 6.14):

@interface WGetParameters : NSObject { NSMutableDictionary *data;

NSMutableString *cmdLine;

}

- (NSString *)getValue:(NSString *)key;

- (void)setValue:(NSString *)key: (NSString *)value;

- (void)printData;

- (NSMutableArray *)getData;

- (NSMutableString *)getCommandLine;

- (void)formatCommandLine;

- (void)saveData:(NSString *)fname;

- (void)loadData:(NSString *)fname;

- (void)initToDefaults;

@end

The class contains two data members: one holds the data state of the program (the model) implemented as an NSMutableDictionary, and the other holds the command line. NSMutableDictionary (mutable meaning capable or subject to change) is part of the Cocoa Foundation collection classes. It holds objects as key/

value associations, similar to a map in the C++ Standard Template Library (STL) or a hash in Perl. For each unique key, there is an associated value. The WGetParameters

Figure 6.14

The WGetParameters class holds data (the model) using an NSMutableDictionary, which stores objects in key/value pairs.

class uses the dictionary to store each command-line parameter and, if necessary, its corresponding value.

The WGetParameters class initializes its data members through its init method. The init method, like a C++ constructor, is called when the class is instantiated by the runtime system and is typically used for initializing the class’s data members. Rather than setting the hash table values within the init method, you send a message to initToDefaults and have it set the values to their defaults.

You do this so the program can reuse the method to respond to the user selecting the Reset button, which also sets the model to its default values. In addition to the dictionary, the class contains an NSMutableString data member, which holds the command-line version of the current state of the model.

Let’s look at the most important class methods:

getValue and setValue—Enable controlled access to the class and therefore the model. The getValue method takes a key parameter that it uses to look up and return the associated value in the model. The setValue method takes two parameters (a key and value) that the class uses to set the value for the associated key.

getData—Responsible for taking the current model state and returning an array of each set key/value pair. By set, I mean a parameter selected by the user in the interface. For example, when the program starts, the data model is set to default values. When the user clicks the Download button, the program controllers query each view, update the model based on the state of the view, and send a getData message to the model, which it responds to by returning the selected command-line parameters. The format- CommandLine method uses the getData method to get the current parameters and convert them to a wget command-line representation.

saveData and loadData—Handle saving the current model to a file, loading a saved file, and populating the model with the stored values. These are two of the more interesting methods. The program uses them to handle this feature. The scenario feature enables the user to save the current setting to a file they can load later. As you can see from the following snippet, the saveData method is only one line—this is all it takes to save the contents of an NSMutableDictionary to a file:

- (void)saveData:(NSString *)fname {

[data writeToFile:fname atomically:YES];

}

- (void)loadData:(NSString *)fname

{

NSString *s = fname;

s = [s stringByExpandingTildeInPath];

[s retain];

[data release];

data = [[NSMutableDictionary alloc] initWithContentsOfFile:s];

if (data == nil) {

data = [[NSMutableDictionary alloc] init];

[self initToDefaults];

} }

The first parameter is the name of the file. The second is a Boolean flag that tells the method how to save the data. If it is YES, the method saves the data to a temporary file and, if successful, copies that file over the named file. If NO, the method writes the data directly to the specified file without the temporary copy. (Temporary copies protect the user if the power is cut while the file is being written.) The format of the files is XML. Here’s an edited example of the XML output:

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

<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/

PropertyList.dtd">

<plist version="0.9">

<dict>

<key>--accept=</key>

<string></string>

<key>--append-output=</key>

<string></string>

<key>--backup-converted</key>

<string>0</string>

<key>--base=</key>

<string></string>

<key>extra-commands</key>

<string></string>

<key>raw-command</key>

<string></string>

</string>

</dict>

</plist>

loadData—Releases the current model and loads the data from the specified file into a new model. If there is an error (data == nil), the method sets the model to its default values.

stringByExpandingTildeInPath—Expands a path name that contains ~ (the user’s home directory) to a full path name.

Collectively, these methods show how easy it is to serialize data to and from disk.

6.6.2 The view

In the MVC paradigm, the view is responsible for displaying data to the user.

When you’re creating CocoaWGet’s user interface within Interface Builder, you effectively created the application view. Cocoa’s Application Kit handles most of the displaying and updating of the view for you.

6.6.3 The controller

The controller is responsible for mediating interaction between the application’s model and view. In the CocoaWGet program, one main controller and four sub- controllers handle this aspect of the pattern.

CocoaWGetController class

The CocoaWGetController class is the main application controller. It is responsible for routing messages to each of the subcontrollers and handling user interaction with the main application. The application supports actions such as saving and opening the current parameters, resetting the interface and model, and invoking a wget retrieval. Here is the interface of the CocoaWGetController class:

@interface CocoaWGetController : NSObject {

IBOutlet id downloadController;

IBOutlet id htmlFtpController;

IBOutlet id limController;

IBOutlet id retrievalController;

IBOutlet NSTextView *theStatus;

IBOutlet NSTextField *url;

IBOutlet NSWindow *mainWindow;

IBOutlet NSWindow *downloadWindow;

NSString *directory;

WGetParameters *param;

}

- (IBAction)handleDownload:(id)sender;

- (IBAction)handleOpen:(id)sender;

- (IBAction)handleReset:(id)sender;

- (IBAction)handleSave:(id)sender;

- (IBAction)handleViewParams:(id)sender;

- (void)reset:(id)sender;

- (void)raiseSheet;

- (void)closeSheet:(id)sender;

- (void)displayCmdLine:(NSString *)headerStr;

@end

The class contains several data members, which correspond to the subcontrollers, interface elements, and model. The subcontrollers’ (downloadController, htmlFtp- Controller, limController, and retrievalController) data members enable Cocoa- WGetController to access each of the subcontrollers. You set the connection between CocoaWGetController and these controllers in Interface Builder by Control-dragging from the CocoaWGetController instance to each subcontroller instance and selecting the corresponding outlet. With these connections intact, the CocoaWGetController can talk to any of the subcontrollers.

The CocoaWGetController class uses the next data members, theStatus and url, to access interface elements—in this case, the status and URL fields. The status field holds the output messages from the wget program, as well as any status infor- mation messages inserted by the CocoaWGet program. The URL field contains the source URL.

After the user selects options and clicks the Download button, CocoaWGet begins the download process. At this point, the program should inform the user of its operations so the user knows what is going on. This is somewhat different from the way many UNIX programs work. Typically, a UNIX program remains silent, showing messages only when a warning or an error occurs. The idea behind this design choice is that users only need to worry if they see output. Most GUI interfaces instead display the status of the operation to inform the user that the program is functioning and processing their request. CocoaWGet displays a dialog called a Sheet during the download process. (As you’ll recall from chapter 1, Sheets are modal dialog boxes. When an application displays a Sheet, it appears attached to an application’s document or window.)

Sheets are new to Mac OS X. In order for the program to display the Sheet in the correct window, you need to keep a pointer to the window to which the Sheet is attached. To accomplish this, you use the mainWindow data member. This member holds a pointer to the main application window, which is set in Interface Builder by Control-dragging from the CocoaWGetController to the main application window and setting the connection to the mainWindow outlet. This member is used as a parameter to NSApp’s beginSheet method. The downloadWindow data member holds a pointer to the window that the Sheet uses to display the download message. You create this window and form the connection between it and the downloadWindow data member in Interface Builder.

Along with these data members, the class also holds a pointer to the program’s home directory, set in the init method, which it uses as the default location to store downloaded files and the application’s model class (param). The class uses the model object to access the application model.

In addition to the data members, CocoaWGetController implements several methods that enable it to respond to user requests or actions, display application information, and interact with the model. The handle family of methods responds to user requests:

handleSave—Responds to messages to save the currently selected parame- ters to a file. Each of these methods uses support methods defined in the AppSupport class, discussed later in the section.

handleReset—Sets the model to its default values and sends a message to the view to update its display.

handleViewParams—Displays the currently selected parameters in the status field as a wget command line.

handleDownload—The most interesting of the defined methods. It is in charge of collecting and formatting wget parameters and running the wget task to retrieve all files based on user selections:

- (IBAction)handleDownload:(id)sender {

NSMutableArray *args;

MyTask *task;

[self displayCmdLine:@"Downloading files, please wait..."];

[limController getParameters:param];

[downloadController getParameters:param];

[retrievalController getParameters:param];

[htmlFtpController getParameters:param];

[param setValue:@"url":[url stringValue]];

[self raiseSheet];

args = [param getData];

task = [[MyTask alloc] init];

[task runTask:WGET_CMD theDirectory:directory theArgs:args getOutputFrom:1];

[self closeSheet:sender];

[AppSupport setStatusMsgWithDate:theStatus theMsg:[task output]];

[self displayCmdLine:@"Download complete."];

if ([task exitStatus] != 0)

NSRunAlertPanel(@"Error getting files",

@"wget returned an error.", @"OK", NULL, NULL );

[task release];

}

The handleDownload method works as follows:

1 It prints a message to the status text area telling the user that the download is beginning, and updates the application model by sending a message to each subcontroller to query its controls and update the model to reflect the current settings.

2 It sends a message to the raiseSheet method to display the download Sheet, which has the side effect of disabling the interface for user interaction.

3 It sends a message to the model to return the parameters in an array, where each element is a key/value parameter pair.

4 To run the wget task, the method instantiates a MyTask object and sends a message to runTask, passing the launch path to the wget program (/sw/

bin/wget), the directory to store the retrieved file under, the command-line arguments, and where to read the wget output (0 for standard out or 1 for standard error). The MyTask class, which does much of the real work of interacting with the UNIX layer, is discussed later in this section.

5 When the runTask method finishes, the download is complete. handleDown- load closes the Sheet and updates the status field and prints the wget output.

6 The task object is released.

DownloadController, RRController, HtmlFtpController, and LIMController classes

In addition to the main application controller (CocoaWGetController), CocoaWGet uses four subcontrollers that operate under the control of the main controller. As discussed in section 6.5.3, the main program window has four tabbed controls: each pane holds a related group of wget parameters and is controlled by a different controller. The Download pane is mediated by an instance of the DownloadController class; the Recursive Retrieval pane is mediated by an instance of the RRController class; and this pattern continues for the remaining panes and controllers. Each subcontroller implements similar functionality, so let’s look at one of the controllers as an example.

The DownloadController class implements the following data members and methods:

@interface DownloadController : NSObject {

IBOutlet NSTextField *concatFilesTo;

IBOutlet NSTextField *limitDownloadSizeTo;

IBOutlet NSPopUpButton *limitDownloadSizeType;

IBOutlet NSButton *noDirCreateOnDownload;

IBOutlet NSButton *noFilesUnlessNewerThanLocal;

IBOutlet NSButton *noHostnamePrefix;

IBOutlet NSPopUpButton *nRetries;

IBOutlet NSTextField *outputDir;

IBOutlet NSButton *overwriteFiles;

IBOutlet NSPopUpButton *pauseBetweenRetrievals;

IBOutlet NSButton *printServerResponse;

IBOutlet NSButton *proxyOn;

IBOutlet NSPopUpButton *removeNDirComponent;

IBOutlet NSButton *resumeDownload;

IBOutlet NSButton *spider;

IBOutlet NSPopUpButton *waitBetweenFailedRetrievals;

}

- (IBAction)handleSetConcatTo:(id)sender;

- (IBAction)handleSetOutputDir:(id)sender;

- (IBAction)reset:(WGetParameters *)param;

- (void)getParameters:(WGetParameters *)param;

@end

The data members perform functions similar to the other classes we looked at; pri- marily, they provide an access point to get the state of the pane’s interface controls.

You can divide the methods into two categories: methods that react to messages sent by the instance in response to user interface selections, and methods that interact with the data model. The handleSetConcatTo and handleSetOutputDir methods deal with user selections. For example, handleSetOutputDir is responsible for responding when the user clicks the Set button and getting a directory from the user. The method uses the getDirectory methods, which you will implement in the AppSupport class (discussed later in the section):

- (IBAction)handleSetOutputDir:(id)sender {

[outputDir setStringValue:[AppSupport

getDirectory:@"Output Directory"]];

}

The reset method handles updating the interface (view) to reflect the current state of the model. For each interface component, you get the corresponding value from the model and send it as a parameter to the control’s set method:1

- (IBAction)reset:(WGetParameters *)param {

[limitDownloadSizeTo setStringValue:[param getValue:@"--quota="]];

[outputDir setStringValue:[param getValue:

@"--directory-prefix="]];

[pauseBetweenRetrievals setStringValue:[param getValue:

@"--wait="]];

1 The naming scheme of –something= is used to map keys to wget command-line options.

[removeNDirComponent setStringValue:[param getValue:

@"--cut-dirs="]];

[concatFilesTo setStringValue:[param getValue:

@"--output-document="]];

if ([[param getValue:@"--timestamp"] isEqualToString:@"1"]) [noFilesUnlessNewerThanLocal setState:NSOnState];

else

[noFilesUnlessNewerThanLocal setState:NSOffState];

// … }

Let’s look at a few of these statements. To set the value of the Quota text field, you first get the value in the model for the key --quota= and pass it as a parameter to setStringValue. To set the state of a checkbox (NSButton), you determine the key’s value in the model. If it equals 1, the box should be checked, so you send it a setState message with NSOnState as its parameter. Conversely, if the box should be unchecked, you send a setState message with NSOffState as its parame- ter. You repeat this process for each control on the pane.

The getParameters method gets the current user settings from the view and updates the model according to these choices:

- (void)getParameters:(WGetParameters *)param {

NSString *s;

[param setValue:@"--quota=":[limitDownloadSizeTo stringValue]];

[param setValue:@"Q-size":[limitDownloadSizeType titleOfSelectedItem]];

[param setValue:@"--tries=":[nRetries titleOfSelectedItem]];

[param setValue:@"--output-document=":[concatFilesTo stringValue]];

// … }

To set a model’s value, you first get the current value of the control and send a message to the model, passing the options key as the first parameter and the retrieved value as the value parameter. You repeat this process for each control on the pane.

Collectively, these methods, as well as the similar methods in the other subcon- troller classes, work to mediate information between the application’s view and data model, and are managed by CocoaWGetController.

AppSupport support class

CocoaWGet uses two additional classes that provide support functions for the program: AppSupport and MyTask. The AppSupport class, as its name suggests, provides basic support for the program:

@interface AppSupport : NSObject { }

+ (NSString *) getFilename:(NSString *)title;

+ (NSString *) getDirectory:(NSString *)title;

+ (void)setStatusMsgWithDate:(NSTextView *)statusField theMsg:(NSString *)msg;

+ (NSString *)getSaveFile:(NSString *)title;

+ (void)scrollStatus:(NSTextView *)statusField;

@end

The AppSupport class only contains static methods. Static methods are preceded with a +, as opposed to the – character (which indicates an instance method). You use a static method through its class rather than its instance variable, so you can use such methods without creating an instance of the class. This technique is useful in some contexts where you want to provide functionality but do not need to main- tain class state. The following example demonstrates the syntax for an instance method and a factory method:

// Instance method - (void)foo;

// Factory method + (void)foo;

The getFilename, getDirectory, and getSaveFile methods prompt the user for filenames the program uses in various tabbed panes. Each method takes one parameter: the title of the dialog. The first two methods (getFilename and get- Directory) use the Application Kit class NSOpenPanel (specifically, the openPanel method), which prompts the user for the name of a file to open. The getSave- File method uses NSSavePanel’s savePanel method. Here’s the AppSupport class’s getFilename method:

+ (NSString *) getFilename:(NSString *)title {

NSString *s = @"";

NSOpenPanel *panel;

int result;

panel = [NSOpenPanel openPanel];

[panel setCanChooseFiles:TRUE];

[panel setCanChooseDirectories:FALSE];

[panel setAllowsMultipleSelection:FALSE];

[panel setTitle:title];

result = [panel runModalForDirectory:NSHomeDirectory()

file:nil types:nil];

if(result == NSOKButton) {

NSArray *retArray = [panel filenames];

s = [NSString stringWithFormat:@"%@", [retArray objectAtIndex:0]];

}

return s;

}

By changing the parameters of the openPanel method (bold in the listing), you can alter the behavior of the displayed dialog box. For example, getFilename only needs a single filename from the user, so you set setCanChooseFiles to TRUE and setCanChooseDirectories and setAllowsMultipleSelection to FALSE. The getDi- rectory method prompts the user for a directory name, so you set setCanChoose- Files and setAllowsMultipleSelection to FALSE, and setCanChooseDirectories to TRUE. Both methods return either the file or directory name as an NSString. MyTask support class

The MyTask class is one of the more interesting classes in the project. This class is responsible for running a task (program), collecting the results of the run, and returning the result to the user. Is uses the Foundation class NSTask class to do so.

The NSTask class facilitates running a program as a subprocess of the active program, as well as monitoring and interacting with the execution of the subpro- cess. In a sense, this is similar to the UNIXfork/exec model of running a child process of a parent. With NSTask, there are two ways to run subprocess: you can run the process in the environment it inherits from its creator process or use the NSTasklaunch method. The following example demonstrates how to use the first method in Objective-C:

NSTask *task = [NSTask launchedTaskWithLaunchPath:path arguments:argumentArray];

NSLog("task returned: %@", [task terminationStatus];

You launch a subtask using the launchedTaskWithLaunchPath method, which takes two arguments: the absolute path to the process you wish to run and any argu- ments you wish to pass to the process. For example, to use this call to run wget, the path parameter would hold the absolute path to the wget program, and the argument parameter would hold any wget command-line arguments. Note that the subprocess inherits its runtime environment from the calling process. In addition, launchedTaskWithLaunchPath is a static method, so there is no need to instantiate the NSTask class. When the call returns, you can use the returned NSTask object to interact with the task.

Một phần của tài liệu Programming mac OS x a guide for unix developers (Trang 247 - 260)

Tải bản đầy đủ (PDF)

(385 trang)