Stone Design Stone Design
News Download Buy Software About Stone
software

OS X & Cocoa Writings by Andrew C. Stone     ©1995-2003 Andrew C. Stone

DOCtor - Wrapping UNIX CLI's into GUIs.
©2002 Andrew C. Stone All Rights Reserved

With OS X comes a wealth of UNIX command line programs to accomplish all sorts of tasks. Wrapping these CLI's into a graphical user interface that Mac users expect and even demand can quickly turn this fortune into usable programs. This month I'll provide the complete source to the graphical user interface and CLI glue for "DOCtor" - an application to convert Microsoft Word .doc documents into Mac OS X viewable PDF. Although this program calls the UNIX executable "antiword", it provides a framework skeleton which could be used on any executable. You will learn how to:
     - modify UNIX programs to work in a GUI
     - launch and log the output of a UNIX program
     - accept drag and drops on a window
     - communicate with another program via its API
     - extend NSFilemanager via categories
     - open URLs programmatically

Full source code to both DOCtor and antiword is available at our web site at:
http://www.stone.com/DOCtor/DOCtor.tar.gz.    
    
    
Is there a DOCtor in the house?

Proprietary file formats are annoying and almost useless to the ever-growing number of people who refuse to use this class of software. The GNU (GNU means "GNU Not UNIX", recursively) community is a diverse, global collection of forward thinking programmers who produce code that is protected under the GNU Foundation's "Copyleft". Instead of taking away the user's rights, it guarantees that others can copy and use the code, as long they continue to give it away complete and free.

When the PStill™ engine author, Frank Siegert, informed me of the antiword project, I thought it would be useful to add a GUI and more functionality to this UNIX program. Antiword converts Word documents into PostScript. While PS can be printed, it cannot be viewed directly under Mac OS X. DOCtor calls PStill's published application programmer's interface (API) to distill the PS into PDF. It then passes the PDF to another program for display. If Stone Design's Create® is installed, DOCtor will show the file in Create; otherwise, it will use the user's default PDF viewer (Preview, for example).

Call the DOCtor!

The design of a GUI for a UNIX executable is really a task of mapping the possible parameters of the command line program to user interface elements. Here's the output from running antiword from the command line without any parameters:

[ibis:AntiWord/DOCtor/antiword] andrew% ./antiword
Name: antiword
Purpose: Display MS-Word files
Author: (C) 1998-2001 Adri van Os
Version: 0.32 (05 Oct 2001)
Status: GNU General Public License
Usage: antiword [switches] wordfile1 [wordfile2 ...]
Switches: [-t|-p papersize][-m mapping][-w #][-i #][-X #][-Ls]
-t text output (default)
-p <paper size name> PostScript output
like: a4, letter or legal
-m <mapping> character mapping file
-w <width> in characters of text output
-i <level> image level (PostScript only)
-X <encoding> character set (Postscript only)
-L use landscape mode (PostScript only)
-s Show hidden (by Word) text

First, decide which parameters are relevant. We're not interested in the parameters that apply to a simple text translation of the document; we want the flags for PostScript output. Second, decide which parameters the program should support. We will let the user specify the paper type ( -p) and orientation (-L), but leave image level (-i) and character set (-X) for future enhancements. Third, decide how to display the options. I often choose to simplify the interface by hiding expert options in drawers. This way, the "easy" interface is simple and clean, but experts have the options at their fingertips:

The good DOCtor's interface is simple - but exposes more options for the expert in the drawer

The Classes and Their Responsibilities

The DOCtor is composed of 3 "brain" classes, 2 user interface subclasses, and 2 category extensions. The basic design is a central controller that coordinates the user interface with the antiword process.


The Brain Classes: DRController, ConversionJob and SDLogTask

The mapping between the user interface and antiword is handled by our NSApplication's delegate class, DRController. DRController is responsible for accepting input from the UI, handling drag and drops, and providing feedback on the process to the user. It also manages what happens after antiword has successfully translated the .doc into PostScript, including calling the PStill™ API.

Each translation job is represented by an instance of ConversionJob - this neatly wraps up the options into the instance variables of this class. ConversionJob's responsibilities are to create an argument array and launch and monitor the antiword UNIX process. ConversionJob also determines what files can be translated - note the use of NSFileTypeForHFSTypeCode('WDBN') which allows WORD files without a ".doc" extension to be correctly identified as a WORD document.

The SDLogTask is a reusable wrapper on NSTask that simplifies the calling of a CLI, as well as providing any output from the running of the program to the console window, if its visible. It's a standard addition to many of the Stone applications.


The User Interface Classes: EasyWindow and DragFromWell

The main conversion window is an instance of EasyWindow - a subclass of NSWindow that handles the drag and drop of .doc files. The reason we subclass window is to get cursor change feedback whenever the user drags a relevant file over any part of the window, including the title bar. Instead of including program specific knowledge in this class, EasyWindow asks the NSApp's delegate, DRController to both determine which files are acceptable and what gets done when the file is dropped. This is a good strategy for reuse of UI components - concentrate the logic in the controller, and make the UI components as versatile as possible.

When the file conversion is complete, an instance of DragFromWell, an NSImageWell subclass, displays the output file's icon. DragFromWell also allows you to copy the file to the desktop or other Finder location, as well as show you how to implement a "Reveal In Finder". Finally, it passes on "drags" to the window, so that all drag and drop code is centralized. The DragFromWell is also a standard Stone component.

Category Helpers

Categories are additional or replacement methods on an existing Objective C class. This language feature lets you add power to Apple's Frameworks, and are very suited to reuse.

NSFileManager(NSFileManager_Extensions) knows how to create writable folders, determine a suitable temporary folder, and hand out unique names based on a template, and is a standard addition to all of my applications.

NSFileHandle(CFRNonBlockingIO), written by Bill Bumgarner, provides some fixes and additions to the Foundation class NSFileHandle.


Making the Application Tile Accept Drags

The simplest way to use DOCtor is to drag a file onto the DOCtor's application icon in the Dock or Finder. By implementing one simple method in the NSApplication's delegate class, you add this functionality:

- (BOOL)application:(NSApplication *)sender openFile:(NSString *)path
{
return [self convertAndOpenFile:path];
}

There is one more thing you have to do - set up the acceptable document types in Project Builder's Target inspector, Application Settings pane:

Setting the Document Types alerts Finder to what sorts of documents you can open


The Cool Stuff

Modifying antiword

I wanted to use the standard distribution of Adri van Os's antiword (http://www.winfield.demon.nl/index.html), as ported to Mac OS X by Ronaldo Nascimento. There was a glitch however: antiword expects its resource files to be in a certain location, which makes a simple installation difficult. I opted for minimal changes that wouldn't affect the base distribution. By adding a _macosx define in the makefile and redefining GLOBAL_ANTIWORD_DIR to "." (the current directory) in antiword.h, we add support for having the resource files local to the executable, wherever the user installs the application. In conjunction with this change, we also need to set an environment variable $HOME when calling antiword from SDLogTask:

[[SDLogTask alloc]initAndLaunchWithArgs:[self arguments]
    executable:[antiword stringByAppendingPathComponent:@"antiword"]
directory:antiword logToText:[[NSApp delegate] logText]
    includeStandardOutput:NO outFile:outputPath owner:self
    environment:[NSDictionary dictionaryWithObjectsAndKeys:antiword, @"HOME",nil]];

For the most part, standard Unix executables will not require any code tweaking to be incorporated into your application.


Remote methods

Moving data between applications is pretty easy, and getting a quick connection to an application via low level mach calls can avoid the overhead of the extremely easy to use NSConnection method rootProxyForConnectionWithRegisteredName:. See lookupPStillDOServer() for usage of bootstrap_look_up() and connectionWithReceivePort:. Don't forget to assign the proxy its "knowledge":

// in order for a remote object to respond to methods, it needs to be told what it will respond to!
[theProxy setProtocolForProxy:@protocol(PStillVended)];

Once you have this rootProxy connection with PStill, you can access the Objective C methods of the PStillVended protocol in PStill. For example to convert an eps file to a pdf file:

    if ([theProxy convertFile:inputFile toPDFFile:outputFile deleteInput:NO])
        // success!
        
        
Then, to view the PDF, we first try the Stone Studio's Create - otherwise, we let NSWorkspace figure out the user's default application for viewing PDF:

    NSWorkspace *wm = [NSWorkspace sharedWorkspace];
    if (([[NSUserDefaults standardUserDefaults] boolForKey:@"DontOpenInCreate"]) || ![wm openFile:pdfFile withApplication:@"Create" andDeactivate:YES]) {
        return [wm openFile:pdfFile];
    }


Also, note use of NSWorkspace's openURL: - this makes the launching of the user's favorite browser to a given URL "just work":

[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.stone.com/"]];



The Code

antiword 0.32 diffs:

diff -r -C2 antiword.0.32/Makefile.MacOSX antiword.0.32-gui/Makefile.MacOSX
*** antiword.0.32/Makefile.MacOSX Wed Oct 24 18:38:04 2001
--- antiword.0.32-gui/Makefile.MacOSX Tue Nov 13 10:20:09 2001
***************
*** 9,13 ****
DB = NDEBUG
# Optimization: -O<n> or debugging: -g
! OPT = -O2

LDLIBS =
--- 9,13 ----
DB = NDEBUG
# Optimization: -O<n> or debugging: -g
! OPT = -O2 -D__macosx

LDLIBS =
diff -r -C2 antiword.0.32/antiword.h antiword.0.32-gui/antiword.h
*** antiword.0.32/antiword.h Tue Sep 25 17:36:47 2001
--- antiword.0.32-gui/antiword.h Tue Nov 13 10:18:34 2001
***************
*** 143,146 ****
--- 143,150 ----
#define ANTIWORD_DIR "antiword"
#define FONTNAMES_FILE "fontname.txt"
+ #elif defined(__macosx)
+ #define GLOBAL_ANTIWORD_DIR "."
+ #define ANTIWORD_DIR "antiword"
+ #define FONTNAMES_FILE "fontnames"
#else
#define GLOBAL_ANTIWORD_DIR "/opt/antiword/share"


//////// DRController.h ///////

#import <Cocoa/Cocoa.h>

@interface DRController : NSObject
{
IBOutlet id statusField;
IBOutlet id well;
IBOutlet id window;

IBOutlet id portraitLandscapeMatrix;
IBOutlet id paperSizePopUp;
IBOutlet id openInCreateSwitch;

IBOutlet id textLogView;    // the console

IBOutlet id drawer;

IBOutlet id tabView;    // GPL license and other help info
}

// exposed API for drag and drops:
- (BOOL)convertAndOpenFile:(NSString *)docFile;

// the callback after the antiword executable terminates:
- (void)taskTerminated:(BOOL)success outputFile:(NSString *)outpout;


// IB actions - open MainMenu.nib in IB to see what's connected to what:

// menu items:
- (IBAction)gotoStoneSite:(id)sender;
- (IBAction)showLogAction:(id)sender;
- (IBAction)showGPLAction:(id)sender;
- (IBAction)showSourceAction:(id)sender;
- (IBAction)showDOCtorSourceAction:(id)sender;


// ui items:

- (IBAction)changeOpenInCreateAction:(id)sender;
- (IBAction)toggleDrawer:(id)sender;

// our console text:
- (id)logText;

@end


//////// DRController.m ///////

#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#import <stdio.h>
#import <mach/mach.h>
#import <servers/bootstrap.h>
#include <unistd.h>
#import "DRController.h"
#import "ConversionJob.h"
#import "NSFileManager-Extensions.h"
#import "DragFromWell.h"


// the first part of this file contains the routines needed to ask another Cocoa app
// to do something. In this case, we ask PStill to convert a PostScript file into PDF.

#define PStillFilterServer ([NSString \
             stringWithFormat:@"PStillFilterServer-%@",\
             [[NSProcessInfo processInfo] hostName]])

@protocol PStillVended

// 0 = succes anything else = failure
// caller's responsibility to make sure outputFile is writeable and not
// existing!
- (int)convertFile:(NSString *)inputFile
toPDFFile:(NSString *)outputFile deleteInput:(BOOL)deleteInput;

// you can also convert many files to one single PDF with this one:
- (int)convertFiles:(NSArray *)fullPathsToFiles
toPDFFile:(NSString *)outputFile deleteInput:(BOOL)deleteInput;

// when licensing is done, this will return YES, otherwise NO
- (BOOL)applicationReadyForInput;

// for jobs to show a preview from a thread
- (void) showImageFile:(NSString *)file width:(int)width height:(int)height
isEPS:(BOOL)isEPS rotation:(int)rotation pageNumber:(int)pageNumber;

@end

#define USAGE NSLog(@"This Print Filter requires PStill for Mac OS X by Stone Design and Frank Siegert. Visit www.stone.com to download and full information.")

#define WAKE_UP_WAIT    5

static id<PStillVended> lookupPStillDOServer(void) {
port_t sendMachPort;
NSDistantObject *rootProxy = nil;
id<PStillVended> result;

// First, try look up PStill;s DO object in the bootstrap server.
// This is where the app registers it by default.
if ((BOOTSTRAP_SUCCESS ==
     bootstrap_look_up(bootstrap_port,
             (char *)([PStillFilterServer cString]),
             &sendMachPort)) && (PORT_NULL != sendMachPort)) {
    NSConnection *conn = [NSConnection
             connectionWithReceivePort: [NSPort port]
             sendPort:[NSMachPort
                    portWithMachPort:sendMachPort]];
    rootProxy = [conn rootProxy];
}

// If the previous call failed, the following might succeed if the user
// logged in is running Terminal with the PublicDOServices user default
// set.
if (!rootProxy) {
    rootProxy = [NSConnection rootProxyForConnectionWithRegisteredName:
         PStillFilterServer host:@""];
}

// We could also try to launch PStill at this point, using
// the NSWorkspace protocol.

if (!rootProxy) {
    if (![[NSWorkspace sharedWorkspace] launchApplication:@"PStill"]) {
     USAGE;
     return nil;
    }
    sleep(WAKE_UP_WAIT);
    rootProxy = [NSConnection rootProxyForConnectionWithRegisteredName:PStillFilterServer host:@""];
}

if (!rootProxy) {
    fprintf(stderr,"Can't connect to PStill\n");
    return nil;
}

[rootProxy setProtocolForProxy:@protocol(PStillVended)];
result = (id<PStillVended>)rootProxy;
return result;
}

BOOL convertFiles(NSArray *inputFiles, NSString *outputFile) {
id<PStillVended> theProxy = lookupPStillDOServer();
// if we can't find it, bail:
if (theProxy != nil) {
    // if she's not launched, we wait until she's licensed or they
    // give up on licensing:
    while (![theProxy applicationReadyForInput]) {
     [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
    }

    if (([inputFiles count]==1 && [theProxy convertFile:[inputFiles objectAtIndex:0] toPDFFile:outputFile
     deleteInput:NO] == 0)) {
     return YES;
    } else if ([theProxy convertFiles:inputFiles toPDFFile:outputFile
     deleteInput:NO]) {
     return YES;
} else {
     NSLog(@"Couldn't convert %@",[inputFiles objectAtIndex:0]);
    }
}
else {
    NSLog(@"Couldn't connect to PStill");
}
return NO;
}

@implementation DRController


// Launching a URL in the user's favorite browser is this simple in Cocoa:

- (IBAction)gotoStoneSite:(id)sender {
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.stone.com/"]];
}

- (IBAction)showDOCtorSourceAction:(id)sender {
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.stone.com/DOCtor/"]];
}

// when the Application launches, set the state of the interface to match the user's preferences:
- (void)awakeFromNib {
[openInCreateSwitch setState:![[NSUserDefaults standardUserDefaults] boolForKey:@"DontOpenInCreate"]];
}

- (IBAction)changeOpenInCreateAction:(id)sender;
{
[[NSUserDefaults standardUserDefaults] setBool:![openInCreateSwitch state] forKey:@"DontOpenInCreate"];
}

- (IBAction)showGPLAction:(id)sender {

// In Interface Builder, you can set the identifier on each tab view
// this allows you to programmatically select a given Tab:
[tabView selectTabViewItemWithIdentifier:@"GPL"];

// Don't forget to order the window front in case it's hidden:
[[tabView window] makeKeyAndOrderFront:self];
}


- (void)revealInFinder:(NSString *)path {
    BOOL isDir;
    if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]) {
if (isDir)
[[NSWorkspace sharedWorkspace] selectFile:nil inFileViewerRootedAtPath:path];
else
[[NSWorkspace sharedWorkspace] selectFile:path inFileViewerRootedAtPath:nil];
    }
}

- (IBAction)showSourceAction:(id)sender {
NSString *antiword = [[[NSBundle mainBundle]pathForResource:@"antiword" ofType:@""]stringByAppendingPathComponent:@"antiword.0.32.tar.gz"];
[self revealInFinder:antiword];

}

- (IBAction)toggleDrawer:(id)sender {
if ([drawer state]== NSDrawerClosedState) [drawer openOnEdge:NSMinYEdge];
else if ([drawer state] == NSDrawerOpenState) [drawer close:nil];
}

- (IBAction)showLogAction:(id)sender {
[[textLogView window] makeKeyAndOrderFront:self];
}

// drag & drop routines
// a standard way to name output files, based on an input name:

- (NSString *)fileForInput:(NSString *)docFile extension:(NSString *)extension {
NSFileManager *fm = [NSFileManager defaultManager];
return [fm nextUniqueNameUsing:[[fm temporaryDirectory] stringByAppendingPathComponent:[[[docFile lastPathComponent] stringByDeletingPathExtension]stringByAppendingPathExtension:extension]]];
}

- (NSString *)psFileForDocFile:docFile {
return [self fileForInput:docFile extension:@"ps"];
}

- (NSString *)pdfFileForDocFile:docFile {
return [self fileForInput:docFile extension:@"pdf"];
}

- (BOOL)convertFile:(NSString *)docFile toFile:(NSString *)psFile {
// call antiword with a ConversionJob instance:
ConversionJob *job = [[ConversionJob allocWithZone:[self zone]] initWithInputFile:(NSString *)docFile outputFile:(NSString *)psFile landscape:[portraitLandscapeMatrix selectedTag] paperName:[paperSizePopUp titleOfSelectedItem]];

// pending : QUEUES
// we may go threaded later...
return [job doExecution:self];

}


- (BOOL)openFile:(NSString *)pdfFile {
NSWorkspace *wm = [NSWorkspace sharedWorkspace];
if (([[NSUserDefaults standardUserDefaults] boolForKey:@"DontOpenInCreate"]) || ![wm openFile:pdfFile withApplication:@"Create" andDeactivate:YES]) {
return [wm openFile:pdfFile];
}
return NO;
}


// Ask PStill to do convert the file:

- (BOOL)pstillConvert:(NSString *)ps toFile:(NSString *)pdf {
return convertFiles([NSArray arrayWithObject:ps], pdf);
}


// Once the job finishes, this is called - let's give the user some feedback:

- (void)taskTerminated:(BOOL)success outputFile:(NSString *)outpout {
if (success) {
NSString *pdfFile = [self pdfFileForDocFile:outpout];
[well setFile:outpout];
[statusField setStringValue:@"Success - click image to reveal in Finder"];

// We have a valid PS file - let's begin the next step of translation to PDF:
if ([self pstillConvert:outpout toFile:pdfFile]) {

// success! Let's give some feedback and open the file:
[well setFile:pdfFile];
[self openFile:pdfFile];
} else {
[statusField setStringValue:@"PStill had trouble - see PStill's Log"];
}

} else {
[well setFile:@""];
[statusField setStringValue:@"Converted failed - see Log!"];
}
}

- (id)logText {
return textLogView;
}


- (BOOL)convertAndOpenFile:(NSString *)docFile {
NSString *psFile = [self psFileForDocFile:docFile];
[self convertFile:docFile toFile:psFile];
return YES;
}

// In order for your app to open files that are dragged upon the dock tile, you need to do two things
// 1. Implement - application: openFile: in your NSApplications's delegate class
// 2. In PB, specify valid Document Types in Target inspector, Application settings

- (BOOL)application:(NSApplication *)sender openFile:(NSString *)path
{
return [self convertAndOpenFile:path];
}

@end


//////// ConversionJob.h ///////
//
// ConversionJob.h
// DOCtor
//
// Created by andrew on Thu Apr 12 2001.
//


typedef enum {
    ConversionNeedsInfo,
    ConversionReadyToBegin,
    ConversionBusy,
    ConversionDone,
    ConversionAborted,
    ConversionError
} ConversionStatus;

@interface ConversionJob: NSObject <NSCopying>
{
    NSString *inputPath;
    NSString *outputPath;
    NSString *statusString;
NSString *paperName;
BOOL _isLandscape;
    ConversionStatus conversionStatus;
}


// these class methods let the UI determine what can really be converted:
+ (NSArray *)convertibleFileTypes;
+ (BOOL)canConvertFile:(NSString *)path;

- (void)abort;

// this is ConversionJob's designated initializer:
- (id)initWithInputFile:(NSString *)docFile outputFile:(NSString *)psFile landscape:(BOOL)landscape paperName:(NSString *)paper;


// these represent the various paramters to a conversion:

- (void)setLandscape:(BOOL)bin;
- (BOOL)landscape;

- (NSArray *)arguments;
- (int)doExecution:(id)delegate;

- (NSString *)inputPath;
- (void)setInputPath:(NSString *)path;

- (NSString *)outputPath;
- (void)setOutputPath:(NSString *)path;
- (NSString *)validOutputPath;

- (NSString *)statusString;
- (void)setStatusString:(NSString *)status;

- (NSString *)paperName;
- (void)setPaperName:(NSString *)name;

- (ConversionStatus)conversionStatus;
- (void)setConversionStatus:(ConversionStatus)status;


extern NSString *JobStatusDidChangeNotification;

@end


//////// ConversionJob.m ///////
//
// ConversionJob.m
//
//
// Created by andrew on Thu Apr 12 2001.
// Andrew C. Stone and Stone Design Corp
//

#include <stdio.h>
#include <stdlib.h>
#import <Carbon/Carbon.h>

#import <Cocoa/Cocoa.h>
#import "NSFileManager-Extensions.h"

#import "ConversionJob.h"
#import "DRController.h"
#import "SDLogTask.h"

@implementation ConversionJob

NSString *JobStatusDidChangeNotification = @"JobStatusDidChange";

/*
Usage: antiword [switches] wordfile1 [wordfile2 ...]
Switches: [-t|-p papersize][-m mapping][-w #][-i #][-X #][-Ls]
-t text output (default)
-p <paper size name> PostScript output
like: a4, letter or legal
-w <width> in characters of text output
-i <level> image level (PostScript only)
-m <mapping> character mapping file
-X <encoding> character set (Postscript only)
-L use landscape mode (PostScript only)
-s Show hidden (by Word) text
*/

// We live in a world of both file extensions AND traditional Mac types - so let's look for both:

+ (NSArray *)convertibleFileTypes {
return [NSArray arrayWithObjects:@"doc",@"DOC",NSFileTypeForHFSTypeCode('WDBN'),nil];
}

+ (BOOL)canConvertFile:(NSString *)path {
NSString *extension = [path pathExtension];

// IF there is no extension, let's see if there is a Mac Type:
if (IS_NULL(extension)) extension = NSHFSTypeOfFile(path);

if ([[self convertibleFileTypes] containsObject:extension]) return YES;
return NO;
}


- (id)init {
self = [super init];
if (self) {
inputPath = @"";
outputPath = @"";
statusString = @"";
conversionStatus = ConversionNeedsInfo;
}
return self;
}

// the designated initializer

- (id)initWithInputFile:(NSString *)docFile outputFile:(NSString *)outpath landscape:(BOOL)landscape paperName:(NSString *)paper;
{
self = [self init];

inputPath = [docFile copyWithZone:[self zone]];
outputPath = [outpath copyWithZone:[self zone]];
paperName = [paper copyWithZone:[self zone]];
_isLandscape = landscape;
return self;
}


// Don't Litter!

- (void)dealloc {
[inputPath release];
[outputPath release];
[statusString release];
[paperName release];
[super dealloc];
}

// note that we do not copy status or status string!
- (id)copyWithZone:(NSZone *)zone {
ConversionJob *newJob = [[ConversionJob allocWithZone:zone] init];
[newJob setInputPath:inputPath];
[newJob setOutputPath:outputPath];
[newJob setPaperName:paperName];
[newJob setLandscape:_isLandscape];
return newJob;
}


// sets and gets

- (NSString *)paperName {
return paperName;
}

- (void)setPaperName:(NSString *)path {
if (![path isEqualToString:paperName]) {
[paperName release];
paperName = [path copyWithZone:[self zone]];
[self setConversionStatus:ConversionReadyToBegin];
}
}


- (void)setLandscape:(BOOL)bin {
_isLandscape = bin;
}

- (BOOL)landscape {
return _isLandscape;
}

- (NSString *)inputPath {
return inputPath;
}


- (void)setInputPath:(NSString *)path {
if (![path isEqualToString:inputPath]) {
[inputPath release];
inputPath = [path copyWithZone:[self zone]];
[self setConversionStatus:ConversionReadyToBegin];
}
}

- (NSString *)outputPath {
return outputPath;
}

- (void)setOutputPath:(NSString *)path {
if (![path isEqualToString:outputPath]) {
[outputPath release];
outputPath = [path copyWithZone:[self zone]];
}
}

+ (BOOL)pathValid:(NSString *)path {
BOOL isDir;
return (NOT_NULL(path) && [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir] && isDir && [[NSFileManager defaultManager] isWritableFileAtPath:path]);
}

- (NSString *)defaultOutputFolder:(NSString *)nextToInput {
return [[NSFileManager defaultManager] temporaryDirectory];
}

- (BOOL)canWriteTo:(NSString *)file {
return [[NSFileManager defaultManager]isWritableFileAtPath:[file stringByDeletingLastPathComponent]];
}


// This method will create a valid output path:

- (NSString *)validOutputPath {
static int unique = 0;
if (NOT_NULL(outputPath) && [self canWriteTo:outputPath]) return outputPath;
else {
[self setOutputPath:[[[NSFileManager defaultManager] temporaryDirectory] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@_%d.%@",[[inputPath lastPathComponent] stringByDeletingPathExtension],++unique,[inputPath pathExtension]]]];
    return outputPath;
}
}

- (NSString *)statusString {
if (NOT_NULL(statusString))
return statusString;
else {
switch(conversionStatus) {
case ConversionNeedsInfo:
return NSLocalizedStringFromTable(@"Awaiting Input Font or Folder - drag one on!",@"DOC",@"status string when info is still needed");
case ConversionReadyToBegin:
return NSLocalizedStringFromTable(@"Initializing Job",@"DOC",@"status string when job starts");
case ConversionBusy:
return [NSString stringWithFormat:@"%@ %@...",NSLocalizedStringFromTable(@"Converting",@"DOC",@"status string when conversion is happening"),[inputPath lastPathComponent]];
case ConversionDone:
return NSLocalizedStringFromTable(@"Conversion complete - drag it out!",@"DOC",@"status string when conversion is done");
case ConversionAborted:
return NSLocalizedStringFromTable(@"Conversion was stopped",@"DOC",@"status string when conversion is stopped");
     case ConversionError:
     default:
     return NSLocalizedStringFromTable(@"ERROR! See Log...",@"DOC",@"status string when error converting");

}
}
}

- (void)abort {
// PENDING: kill the job!
[self setConversionStatus:ConversionAborted];
}

- (void)setStatusString:(NSString *)status {
if (![status isEqualToString:statusString]) {
    [statusString release];
statusString = [status copyWithZone:[self zone]];
}
}


- (ConversionStatus)conversionStatus {
return conversionStatus;
}

- (void)setConversionStatus:(ConversionStatus)status {
conversionStatus = status;
[[NSNotificationCenter defaultCenter] postNotificationName:JobStatusDidChangeNotification object:self];
}


- (NSArray *)arguments {
NSMutableArray *args = [NSMutableArray array];

[args addObject:@"-p"];
[args addObject:paperName];

if (_isLandscape) [args addObject:@"-L"];

[args addObject:inputPath];

// future options can be added here...

return args;
}

//
// Here is the callback from SDLogText
// We'll pass it on up to DRController
//

- (void)taskTerminated:(BOOL)success
{
[[NSApp delegate] taskTerminated:success outputFile:outputPath];
}



- (int)doExecution:(id)delegate {
volatile int statusAtCompletion = -1;

// we place each execution inside of it's own NSAutoreleasePool
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

// the actual executable is inside the folder 'antiword'
NSString *antiword = [[NSBundle mainBundle]pathForResource:@"antiword" ofType:@""]; // the folder!

[self setConversionStatus:ConversionBusy];

[self validOutputPath];


// Here's my groovy Task wrapper which logs errors:
// the one interesting variable is the dictionary passed into the environment:
// We need to set the HOME (where the resources will be sought by antiword)
//
[[SDLogTask alloc]initAndLaunchWithArgs:[self arguments] executable:[antiword stringByAppendingPathComponent:@"antiword"]
directory:antiword
logToText:[[NSApp delegate] logText] includeStandardOutput:NO outFile:outputPath owner:self environment:[NSDictionary dictionaryWithObjectsAndKeys:antiword, @"HOME",nil]];


[pool release];

return 1;
}


@end


//////// SDLogTask.h ///////
/* SDLogTask.h created by andrew on Thu 30-Apr-1998 */
//
// A wrapper of some intense multi-threaded code
// To allow you to spawn a task and see the errors
//

#import <AppKit/AppKit.h>

@interface SDLogTask : NSObject
{
    id owner;
    NSTextView *logText;
    NSTask *task;
    BOOL includeStandardOutput;
NSString *outFile;
    NSPipe *standardErrorPipe;
    NSPipe *standardOutputPipe;
    NSFileHandle *standardErrorHandle;
NSFileHandle *standardOutputHandle;
NSFileHandle *outputFileHandle;
}

//
// executable is full path to program
// args is an array of strings of the arguments
// send in an NSTextView if you want console behaviour
// includeStandardOut: is YES if you also want stdout logged to Text
//

- (id)initAndLaunchWithArgs:(NSArray *)args executable:(NSString *)pathToExe directory:(NSString *)dir logToText:(NSTextView *)text includeStandardOutput:(BOOL)includeStandardOut outFile:(NSString *)outFile owner:anOwner environment:(NSDictionary *)environment;


+ (void)appendString:(NSString *)string toText:(NSTextView *)tv newLine:(BOOL)newLine;

@end


//////// SDLogTask.m ///////
//
/* SDLogTask.m created by andrew on Thu 30-Apr-1998 */

#import "SDLogTask.h"

// kudos to bbum@codefab.com for helping me fix IO:
#import "NSFileHandle_CFRNonBlockingIO.h"

//
// If you want notification when the task completes
// implement this method in the "owner" class
//

@interface NSObject(SDLogTask_Delegate)
- (void)taskTerminated:(BOOL)success;
@end

@implementation SDLogTask

- (void)dealloc
{
    if (outFile) [outFile release];
    [super dealloc];
}
//
// Methods to make it easy to spew feedback to user:
//

#define END_RANGE NSMakeRange([[tv string]length],0)
+ (void)appendString:(NSString *)string toText:(NSTextView *)tv newLine:(BOOL)newLine;
{
[tv replaceCharactersInRange:END_RANGE withString:string];
if (newLine)
[tv replaceCharactersInRange:END_RANGE withString:@"\n"];
else
[tv replaceCharactersInRange:END_RANGE withString:@" "];

if ([[tv window] isVisible]) {
[tv scrollRangeToVisible:END_RANGE];
}
}

- (void)appendString:(NSString *)string newLine:(BOOL)newLine
{
[[self class] appendString:string toText:logText newLine:newLine];
}

- (void)outputData:(NSData *)data
{
    [self appendString:[[[NSString alloc]initWithData:data encoding:[NSString defaultCStringEncoding]]autorelease] newLine:YES];
}


- (void) processAvailableData;
{
    NSData *data;
    BOOL dataProcessed;
NS_DURING
    dataProcessed = NO;
    data = [standardErrorHandle availableDataNonBlocking];
    if ( (data != nil) && ([data length] != 0) ) {
        [self outputData:data];
        dataProcessed = YES;
    }
NS_HANDLER
    dataProcessed = NO;
NS_ENDHANDLER

NS_DURING

    if (includeStandardOutput) {
        data = [standardOutputHandle availableDataNonBlocking];
        if ( (data != nil) && ([data length] != 0) ) {
            [self outputData:data];
            dataProcessed = YES;
        }
    }
NS_HANDLER
dataProcessed = NO;
NS_ENDHANDLER


    if ([task isRunning] == YES) {
        if (dataProcessed == YES)
            [self performSelector: @selector(processAvailableData)
                withObject: nil afterDelay: .1];
else
        [self performSelector: @selector(processAvailableData)
                withObject: nil afterDelay: 2.5];
    }
}


- (void) outputInfoAtStart:(NSString *)pathToExe args:(NSArray *)args
{
    int i;
    // clear it out from last time:
    //[logText setString:@""];

    [self appendString:pathToExe newLine:NO];
    for (i = 0; i < [args count]; i++)
        [self appendString:[args objectAtIndex:i] newLine:NO];
    [self appendString:@"\n" newLine:YES];
}

- initAndLaunchWithArgs:(NSArray *)args executable:(NSString *)pathToExe directory:(NSString *)dir logToText:(NSTextView *)text includeStandardOutput:(BOOL)includeStandardOut outFile:(NSString *)anOutFile owner:anOwner environment:(NSDictionary *)environment
{

    [super init];
    logText = text;
includeStandardOutput = includeStandardOut;
outFile = [anOutFile copy];
    owner = anOwner;

    standardErrorPipe = [NSPipe pipe];
    standardErrorHandle = [standardErrorPipe fileHandleForReading];

if ((outFile!=nil) && ![outFile isEqualToString:@""]) {
[[NSFileManager defaultManager] removeFileAtPath:outFile handler:nil];
if ([[NSFileManager defaultManager] createFileAtPath:outFile contents:[NSData data] attributes:nil])
outputFileHandle = [NSFileHandle fileHandleForWritingAtPath:outFile];
else {
NSLog(@"Cannot open %@! Aborting...",outFile);
if ([owner respondsToSelector:@selector(taskTerminated:)])
[owner taskTerminated:0];
        return nil;
}
}else if (includeStandardOutput) {
standardOutputPipe = [NSPipe pipe];
standardOutputHandle = [standardOutputPipe fileHandleForReading];
}
    task = [[NSTask alloc] init];

[task setArguments:args];
    [task setCurrentDirectoryPath:dir];
[task setLaunchPath:pathToExe];

    [task setStandardError:standardErrorPipe];

    if (environment != nil) [task setEnvironment:environment];

if (outputFileHandle != nil) [task setStandardOutput:outputFileHandle];
    else if (includeStandardOutput) [task setStandardOutput:standardOutputPipe];

[self outputInfoAtStart:pathToExe args:args];


    [self performSelector: @selector(processAvailableData) withObject: nil afterDelay: 1.0];
    
    // HERE WE GO: spin off a thread to do this:
    [task launch];

    [[NSNotificationCenter defaultCenter]
        addObserver: self
        selector: @selector(checkATaskStatus:)
        name: NSTaskDidTerminateNotification
        object: task];
    return self;
}

#define TASK_SUCCEEDED NSLocalizedStringFromTable(@"Job SUCCEEDED!\n", @"PackIt", "message when task is successful")

#define TASK_FAILED NSLocalizedStringFromTable(@"Job FAILED!\nPlease see log.", @"PackIt", "when the task fails...")


- (void)checkATaskStatus:(NSNotification *)aNotification
{
    int status = [[aNotification object] terminationStatus];
    if (status == 0 /* STANDARD SILLY UNIX RETURN VALUE */) {
        [self appendString:TASK_SUCCEEDED newLine:YES];
    } else {
        [self appendString:TASK_FAILED newLine:YES];
        [[logText window] orderFront:self];
    }
if (outputFileHandle != nil)
        [outputFileHandle release];     // this will close it according to docs

    if ([owner respondsToSelector:@selector(taskTerminated:)])
        [owner taskTerminated:(status == 0)];
}

@end


//////// DragFromWell.h ///////

@interface DragFromWell : NSImageView
{
NSString *path;
}

- (void)setFile:(NSString *)aPath;

@end


//////// DragFromWell.m ///////
//
// DragFromWell.m
//
// Andrew C. Stone and Stone Design Corp
//

#import "DRController.h"
#import "NSFileManager-Extensions.h"
#import "DragFromWell.h"

@implementation DragFromWell

// these two methods let us drag away without ordering the window front first:
- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent {return YES;}
- (BOOL)shouldDelayWindowOrderingForEvent:(NSEvent *)theEvent
{
return YES;     // see NSView.rtfd for full explanation
}

- (unsigned int)draggingSourceOperationMaskForLocal:(BOOL)flag
{
if (flag) return NSDragOperationCopy;
return NSDragOperationCopy|NSDragOperationGeneric|NSDragOperationLink;
}

- (BOOL)copyDataTo:pboard
{
if (path != nil) {
[pboard declareTypes:[NSArray arrayWithObject:NSFilenamesPboardType] owner:self];
[pboard setPropertyList:[NSArray arrayWithObject:path] forType:NSFilenamesPboardType];
return YES;
} else return NO;

}


- (void)setFile:(NSString *)aPath
{
[path autorelease];

if (!aPath || [aPath isEqualToString:@""] || ![[NSFileManager defaultManager] fileExistsAtPath:aPath]) {
path = nil;
[self setImage:nil];
} else {
path = [aPath copy];
[self setImage:[[NSWorkspace sharedWorkspace] iconForFile:path]];
}
[self display];
}


- (void)revealInFinder:(id)sender {
    BOOL isDir;
    if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]) {
if (isDir)
[[NSWorkspace sharedWorkspace] selectFile:nil inFileViewerRootedAtPath:path];
else
[[NSWorkspace sharedWorkspace] selectFile:path inFileViewerRootedAtPath:nil];
    }
}

- (void)mouseDown:(NSEvent *)e
{
if (NOT_NULL(path)) {
NSPoint location;
NSSize size;
NSPasteboard *pb = [NSPasteboard pasteboardWithName:(NSString *) NSDragPboard];
NSTimeInterval endTime, startTime = [e timestamp];
if ([e clickCount] > 1) {
[[NSWorkspace sharedWorkspace] openFile:path];
     return;
}
if ([self copyDataTo:pb]) {
size = [[self image] size];
location.x = ([self bounds].size.width - size.width)/2;
location.y = ([self bounds].size.height - size.height)/2;

[self dragImage:[self image] at:location offset:NSZeroSize event:(NSEvent *)e pasteboard:pb source:self slideBack:YES];
}
// if they do drag, we won't do this:
endTime = [[[self window] currentEvent] timestamp];
if (endTime - startTime < .40)
[self performSelector:@selector(revealInFinder:) withObject:nil afterDelay:0.5];
} else [[NSApp delegate] showLogAction:self];
}

// override drag stuff...
- (void)awakeFromNib
{
[self registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]];
}

- (unsigned int) draggingEntered:sender
{
return [[self window] draggingEntered:sender];
}

- (unsigned int) draggingUpdated:sender
{
return [[self window] draggingUpdated:sender];
}

- (BOOL) prepareForDragOperation:sender
{
return [[self window] prepareForDragOperation:sender];
}

- (BOOL) performDragOperation:(id <NSDraggingInfo>)sender
{
return [[self window] performDragOperation:sender];
}
- (void)concludeDragOperation:(id <NSDraggingInfo>)sender {
[[self window] concludeDragOperation:sender];
}


@end


//////// EasyWindow.h ///////

#import <Cocoa/Cocoa.h>

@interface EasyWindow : NSWindow
{
}
@end


//////// EasyWindow.m ///////
//
#import <AppKit/AppKit.h>
#import "EasyWindow.h"


#import "DRController.h"

@implementation EasyWindow

- (void)awakeFromNib
{
[self registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]];
}


- (unsigned int) draggingEnteredOrUpdated:sender
{
NSPasteboard *pboard = [sender draggingPasteboard];
id source = [sender draggingSource];
NSString *type = [pboard availableTypeFromArray:[NSArray arrayWithObject:NSFilenamesPboardType]];

if ([source respondsToSelector:@selector(window)] && [source window] == self) return NSDragOperationNone;

if (type) {
if ([type isEqualToString:NSFilenamesPboardType]) {
NSArray *filenames = [pboard propertyListForType:NSFilenamesPboardType];
unsigned i = [filenames count];
NSFileManager *fm = [NSFileManager defaultManager];
BOOL isDir;
if (i == 1) {
NSString *filename = [filenames objectAtIndex:0];
if ([fm fileExistsAtPath:filename isDirectory:&isDir] && !isDir)
return NSDragOperationCopy;
}
}
}
return NSDragOperationNone;
}

- (unsigned int) draggingEntered:sender
{
return [self draggingEnteredOrUpdated:sender];
}

- (unsigned int) draggingUpdated:sender
{
return [self draggingEnteredOrUpdated:sender];
}

- (BOOL) prepareForDragOperation:sender
{
return YES;
}

- (BOOL) performDragOperation:(id <NSDraggingInfo>)sender
{
return YES;
}

- (void)concludeDragOperation:(id <NSDraggingInfo>)sender
{
NSPasteboard *pboard = [sender draggingPasteboard];
NSString *type = [pboard availableTypeFromArray:[NSArray arrayWithObject:NSFilenamesPboardType]];
if (type) {
if ([type isEqualToString:NSFilenamesPboardType]) {
NSArray *filenames = [pboard propertyListForType:NSFilenamesPboardType];
unsigned i = [filenames count];
NSFileManager *fm = [NSFileManager defaultManager];
BOOL isDir;
if (i == 1) {
NSString *filename = [filenames objectAtIndex:0];
if ([fm fileExistsAtPath:filename isDirectory:&isDir] && !isDir)
[[self delegate] performSelector:@selector( convertAndOpenFile:) withObject:[filename retain] afterDelay:0.10];
}
}
}
}


@end


//////// NSFileManager_Extensions.h ///////

/* NSFileManager_Extensions.h created by andrew on Tue 18-Aug-1998 */

#import <AppKit/AppKit.h>

@interface NSFileManager(NSFileManager_Extensions)

- (NSString *)temporaryDirectory;
- (NSString *)nextUniqueNameUsing:(NSString *)templatier;
- (BOOL)createWritableDirectory:(NSString *)path;
- (BOOL)fileManager:(NSFileManager *)manager
shouldProceedAfterError:(NSDictionary *)errorDict;

@end

#define UserTemporaryDirectory    @"TemporaryFolder"
#define IS_NULL(s) (!s || [s isEqualToString:@""])
#define NOT_NULL(s) (s && ![s isEqualToString:@""])


//////// NSFileManager_Extensions.m ///////
//
/* NSFileManager_Extensions.m created by andrew on Tue 18-Aug-1998 */

#import "NSFileManager-Extensions.h"

@implementation NSFileManager(NSFileManager_Extensions)


#define FILE_OPERATION_ERROR NSLocalizedStringFromTable(@"File operation error: %@ with file: %@", @"Muktinath", "alert message on failed file operation")

#define PROCEED NSLocalizedStringFromTable(@"Proceed", @"Muktinath", "msg when you cannot rename a file")
#define STOP NSLocalizedStringFromTable(@"Stop", @"Muktinath", "msg when you cannot rename a file")


- (BOOL)fileManager:(NSFileManager *)manager
shouldProceedAfterError:(NSDictionary *)errorDict
{
int result;
result = NSRunAlertPanel([[NSProcessInfo processInfo] processName],
FILE_OPERATION_ERROR,PROCEED, STOP, NULL,
[errorDict objectForKey:@"Error"],
[errorDict objectForKey:@"Path"]);

if (result == NSAlertDefaultReturn)
return YES;
else
return NO;
}

//
// Temporary Directory stuff: useful code.
//

BOOL directoryOK(NSString *path)
{
BOOL isDirectory;
NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:path isDirectory:&isDirectory] || !isDirectory) {
NSDictionary *dict = [NSDictionary dictionaryWithObject:
[NSNumber numberWithUnsignedLong:0777]
forKey:NSFilePosixPermissions];
if (![fileManager createDirectoryAtPath:path attributes:dict])
return NO;
}
return YES;
}

NSString * existingPath(NSString *path)
{
while (path && ![path isEqualToString:@""]
&& ![[NSFileManager defaultManager] fileExistsAtPath:path])
path = [path stringByDeletingLastPathComponent];
return path;    
}

NSArray *directoriesToAdd(NSString *path, NSString *existing)
{
NSMutableArray *a = [NSMutableArray arrayWithCapacity:4];
if (path != nil && existing != nil) {
while (![path isEqualToString:existing]) {
[a insertObject:[path lastPathComponent] atIndex:0];
path = [path stringByDeletingLastPathComponent];
}
}
return a;
}

// this will go up the path until it finds an existing directory
// and will add each subpath and return YES if succeeds, NO if fails:

- (BOOL)createWritableDirectory:(NSString *)path
{
BOOL isDirectory;
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:path isDirectory:&isDirectory]
&& isDirectory && [fileManager isWritableFileAtPath:path])
return YES; // no work to do
else {
NSString *existing = existingPath(path);
NSArray *dirsToAdd = directoriesToAdd(path,existing);
int i;
BOOL good = YES;
for (i = 0; i < [dirsToAdd count]; i++) {
existing = [existing stringByAppendingPathComponent:
[dirsToAdd objectAtIndex:i]];
if (!directoryOK(existing)) {
good = NO;
break;
}
}
return good;
}
}


- (NSString *)nextUniqueNameUsing:(NSString *)templatier
{
if (![[NSFileManager defaultManager] fileExistsAtPath:templatier])
    return templatier;
else {
int unique = 1;
NSString *tempName = nil;
do {
tempName =[NSString stringWithFormat:@"%@_%d.%@",
[templatier stringByDeletingPathExtension],++unique,
[templatier pathExtension]];
} while ([[NSFileManager defaultManager] fileExistsAtPath:tempName]);

return tempName;
}
}

- (NSString *)temporaryDirectory
{
static NSString *tempDir =nil;
NSString *s = [[NSUserDefaults standardUserDefaults] stringForKey:UserTemporaryDirectory];

if (tempDir == nil || (NOT_NULL(s) && ![tempDir isEqualToString:s])) {

if (NOT_NULL(s)) tempDir = [s retain];
    else tempDir =[[NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@",[[NSProcessInfo processInfo] processName],NSUserName()]] retain];
}

if (! [self createWritableDirectory:tempDir]) {
NSLog(@"Couldn't create %@, using %@",tempDir,
NSTemporaryDirectory());
tempDir = NSTemporaryDirectory();
}
return tempDir;
}


@end


//////// NSFileHandle_NonBlockingIO.h ///////
//
/*
**
** File: NSFileHandle_NonBlockingIO.h
**
** Created by: bbum
**
**
** Standard disclaimer here.
**
** This code may not be correct. It may not even work [but it sure seems to].
** Potentially, it may corrupt your data irretrievably [if it does,
** please let us know-- we will do our best to make sure the same thing
** doesn't happen to us or anyone else].
*/

@interface NSFileHandle (CFRNonBlockingIO)
- (NSData *)availableDataNonBlocking;

- (NSData *)readDataToEndOfFileNonBlocking;
- (NSData *)readDataOfLengthNonBlocking:(unsigned int)length;
@end


//////// NSFileHandle_NonBlockingIO.m ///////
/*
**
** File: NSFileHandle_NonBlockingIO.m
**
** Created by: bbum
**
**
** Standard disclaimer here.
**
** This code may not be correct. It may not even work [but it sure seems to].
** Potentially, it may corrupt your data irretrievably [if it does,
** please let us know-- we will do our best to make sure the same thing
** doesn't happen to us or anyone else].
*/

// platform specific low-level I/O
#ifdef WIN32
#import <System/windows.h>
#elif defined(__MACH__)
#import <libc.h>
#elif defined(__svr4__)
#import <libc.h>
#import <unistd.h>
#import <sys/filio.h>
#endif

// import APIs to NeXT provided frameworks
#import <Foundation/Foundation.h>

// import local framework's API
//#import "CodeFabRuntime.h"

// import API required througout this file's scope
#import "NSFileHandle_CFRNonBlockingIO.h"

@implementation NSFileHandle (CFRNonBlockingIO)
/*"
* Adds non-blocking I/O API to NSFileHandle.
"*/

- (NSData *)availableDataNonBlocking;
/*"
* Returns an NSData object containing all of the currently available data. Does not block if there is no data; returns nil
* instead.
"*/
{
return [self readDataOfLengthNonBlocking: UINT_MAX];
}

- (NSData *)readDataToEndOfFileNonBlocking;
/*"
* Returns an NSData object containing all of the currently available data. Does not block if there is no data; returns nil
* instead. Cover for #{-availableDataNonBlocking}.
"*/
{
return [self readDataOfLengthNonBlocking: UINT_MAX];
}

- (unsigned int) _availableByteCountNonBlocking
{
#ifdef WIN32
HANDLE nativeHandle = [self nativeHandle];
DWORD lpTotalBytesAvail;
BOOL peekSuccess;

peekSuccess = PeekNamedPipe(nativeHandle, NULL, 0L, NULL, &lpTotalBytesAvail, NULL);

if (peekSuccess == NO)
[NSException raise: NSFileHandleOperationException
format: @"PeekNamedPipe() NT Err # %d", GetLastError()];

return lpTotalBytesAvail;
#elif defined(__MACH__) || defined(__svr4__)
int numBytes;
int fd = [self fileDescriptor];

if(ioctl(fd, FIONREAD, (char *) &numBytes) == -1)
[NSException raise: NSFileHandleOperationException
format: @"ioctl() Err # %d", errno];

return numBytes;
#else
#warning Non-blocking I/O not supported on this platform....
abort();
return nil;
#endif
}

- (NSData *)readDataOfLengthNonBlocking:(unsigned int)length;
/*"
* Reads up to length bytes of data from the file handle. If no data is available, returns nil. Does not block.
"*/
{
unsigned int readLength;

readLength = [self _availableByteCountNonBlocking];
readLength = (readLength < length) ? readLength : length;

if (readLength == 0)
return nil;

return [self readDataOfLength: readLength];
}

@end


//////// main.m ///////

#import <Cocoa/Cocoa.h>

int main(int argc, const char *argv[])
{
return NSApplicationMain(argc, argv);
}


Diagnosis

As of this writing, antiword's -i and -X flags are not supported - this would be a nice enhancement for DOCtor to be able to specify alternate encodings and the level of PS compliance. Another useful addition would be to provide a simple text translation using the -t option, as well as having the UI process folders of files instead of just one at a time.

One of Cocoa's great strengths is the ability to quickly wrap traditional command line programs into mere mortal usable graphical user interfaces - I wrote DOCtor in an afternoon, reusing some standard Stone components.

Andrew Stone <andrew@stone.com> is founder of Stone Design Corp <http://www.stone.com/> and divides his time between farming on the earth and in cyperspace.

PreviousTopIndexNext
©1997-2005 Stone Design top