#import "draw.h"

const int DrawVersion = 48; /* minor version of the program */

@implementation DrawApp : Application
/*
* This class is used primarily to handle the opening of new documents
* and other application-wide activity (such as responding to messages from
* the tool palette). It listens for requests from the Workspace Manager
* to open a draw-format file as well as target/action messages from the
* New and Open... menu items. It also keeps the menus in sync by
* fielding the menu items' updateActions.
*/

/* Private C functions used to implement methods in this class. */

static void initMenu(Menu *menu)
/*
* Sets the updateAction for every menu item which sends to the
* First Responder (i.e. their target is nil). When autoupdate is on,
* every event will be followed by an update of each of the menu items
* which is visible. This keep all unavailable menu items dimmed out
* so that the user knows what options are available at any given time.
* Returns the activate menu if is found in this menu.
*/
{
int count;
Matrix *matrix;
MenuCell *cell;
id matrixTarget, cellTarget;

matrix = [menu itemList];
matrixTarget = [matrix target];

count = [matrix cellCount];
while (count--) {
cell = [matrix cellAt:count :0];
cellTarget = [cell target];
if (!matrixTarget && !cellTarget) {
[cell setUpdateAction:@selector(menuItemUpdate:) forMenu:menu];
} else if ([cell hasSubmenu]) {
initMenu(cellTarget);
}
}
}

static DrawDocument *documentInWindow(Window *window)
/*
* Checks to see if the passed window's delegate is a DrawDocument.
* If it is, it returns that document, otherwise it returns nil.
*/
{
id document = [window delegate];
return [document isKindOf:[DrawDocument class]] ? document : nil;
}

static Window *findDocument(const char *name)
/*
* Searches the window list looking for a DrawDocument with the specified name.
* Returns the window containing the document if found.
* If name == NULL then the first document found is returned.
*/
{
int count;
DrawDocument *document;
Window *window;
List *windowList;

windowList = [NXApp windowList];
count = [windowList count];
while (count--) {
window = [windowList objectAt:count];
document = documentInWindow(window);
if ([document isSameAs:name]) return window;
}

return nil;
}

static DrawDocument *openFile(const char *directory, const char *name, BOOL display)
/*
* Opens a file with the given name in the specified directory.
* If we already have that file open, it is ordered front.
* Returns the document if successful, nil otherwise.
*/
{
Window *window;
char buffer[MAXPATHLEN+1], path[MAXPATHLEN+1];

if (name && *name) {
if (!directory) {
directory = ".";
} else if (*directory != '/') {
strcpy(buffer, "./");
strcat(buffer, directory);
directory = buffer;
}
if (!chdir(directory) && getwd(path)) {
strcat(path, "/");
strcat(path, name);
window = findDocument(path);
if (window) {
if (display) [window makeKeyAndOrderFront:window];
return [window delegate];
} else {
return [DrawDocument newFromFile:path andDisplay:display];
}
} else {
NXRunLocalizedAlertPanel(NULL, "Open", "Invalid path: %s", NULL, NULL, NULL, directory, "Alert shown to user when he has tried to open up a file and has specified a file in a directory which is inaccessible. The directory is either unreadable or does not exist for some reason.");
}
}

return nil;
}

static DrawDocument *openDocument(const char *document, BOOL display)
{
char *directory, *name, *ext;
char buffer[MAXPATHLEN+1];

strcpy(buffer, document);
ext = strrchr(buffer, '.');
if (!ext || strcmp(ext, ".draw")) strcat(buffer, ".draw");
name = strrchr(buffer, '/');
if (name) {
*name++ = '\0';
directory = buffer;
} else {
name = buffer;
directory = NULL;
}

return openFile(directory, name, display);
}

/* Public methods */

+ initialize
/*
* Initializes the defaults.
*/
{
const NXDefaultsVector DrawDefaults = {
{ "KnobWidth", "5" },
{ "KnobHeight", "5" },
{ "KeyMotionDelta", "1"},
{ "Quit", NULL },
{ "RemoteControl", "YES" },
{ "HideCursorOnMove", NULL },
{ NULL, NULL }
};

NXRegisterDefaults("Draw", DrawDefaults);

return self;
}

+ new
/*
* setAutoupdate:YES means that updateWindows will be called after
* every event is processed (this is how we keep our inspector and
* our menu items up to date).
*/
{
self = [super new];
[self setAutoupdate:YES];
return self;
}

/* General application status and information querying/modifying methods. */

- currentGraphic
/*
* The current factory to use to create new Graphics.
*/
{
return currentGraphic;
}

- (DrawDocument *)currentDocument
/*
* The DrawDocument in the main window (dark gray title bar).
*/
{
return documentInWindow(mainWindow);
}

- (const char *)currentDirectory
/*
* Directory where Draw is currently "working."
*/
{
const char *cdir = [[self currentDocument] directory];
return (cdir && *cdir) ? cdir : (haveOpenedDocument ? [[OpenPanel new] directory] : NXHomeDirectory());
}

/*
* Call these to enter/exit the TextGraphic tool.
* These are used currently by the Undo architecture.
*/

- startEditMode
{
[tools selectCellAt:1 :0];
[tools sendAction];
return self;
}

- endEditMode
{
[tools selectCellAt:0 :0];
[tools sendAction];
return self;
}

/* Application-wide shared panels */

static const char *cleanTitle(const char *menuItem, char *realTitle)
/*
* Just strips off trailing "..."'s.
*/
{
int length = menuItem ? strlen(menuItem) : 0;

if (length > 3 && !strcmp(menuItem+length-3, "...")) {
strcpy(realTitle, menuItem);
realTitle[length-3] = '\0';
return realTitle;
} else if (length > 1 && (menuItem[length-1] == '\274')) {
strcpy(realTitle, menuItem);
realTitle[length-1] = '\0';
return realTitle;
} else {
return menuItem;
}
}

- (SavePanel *)saveToPanel:sender
/*
* Returns a SavePanel with the accessory view which allows the user to
* pick which type of file she wants to save. The title of the Panel is
* set to whatever was on the menu item that brought it up (it is assumed
* that sender is the menu that has the item in it that was chosen to
* cause the action which requires the SavePanel to come up).
*/
{
char title[100];
const char *theTitle;
SavePanel *savepanel = [SavePanel new];

if (!savePanelAccessory) {
[self loadNibSection:"SavePanelAccessory.nib" owner:self withNames:NO fromZone:[savepanel zone]];
}
theTitle = cleanTitle([[sender selectedCell] title], title);
if (theTitle) [savepanel setTitle:theTitle];
[savepanel setAccessoryView:savePanelAccessory];
[spamatrix selectCellAt:0 :0];
[savepanel setRequiredFileType:"draw"];

return savepanel;
}

- (SavePanel *)saveAsPanel:sender
/*
* Returns a regular SavePanel with "draw" as the required file type.
*/
{
char title[100];
const char *theTitle;
SavePanel *savepanel = [SavePanel new];

[savepanel setAccessoryView:nil];
theTitle = cleanTitle([[sender selectedCell] title], title);
if (theTitle) [savepanel setTitle:theTitle];
[savepanel setRequiredFileType:"draw"];

return savepanel;
}

- (GridView *)gridInspector
/*
* Returns the application-wide inspector for a document's grid.
* Note that if we haven't yet loaded the GridView panel, we do it.
* The instance variable gridInspector is set in setGridInspector:
* since it is set as an outlet of the owner (self, i.e. DrawApp).
*/
{
if (!gridInspector) {
NXZone *zone = NXCreateChildZone([self zone], vm_page_size, vm_page_size, YES);
NXNameZone(zone, "GridView");
[self loadNibSection:"GridView.nib" owner:self withNames:NO fromZone:zone];
NXMergeZone(zone);
}
return gridInspector;
}

- (Panel *)inspectorPanel
/*
* Returns the application-wide inspector for Graphics.
*/
{
if (!inspectorPanel) {
NXZone *zone = NXCreateChildZone([self zone], vm_page_size, vm_page_size, YES);
NXNameZone(zone, "Inspector");
[self loadNibSection:"InspectorPanel.nib" owner:self withNames:NO fromZone:zone];
[inspectorPanel setFrameAutosaveName:"Inspector"];
[inspectorPanel setBecomeKeyOnlyIfNeeded:YES];
[[inspectorPanel delegate] preset];
NXMergeZone(zone);
}
return inspectorPanel;
}

- (DrawPageLayout *)pageLayout
/*
* Returns the application-wide DrawPageLayout panel.
*/
{
static DrawPageLayout *dpl = nil;

if (!dpl) {
dpl = [DrawPageLayout new];
[self loadNibSection:"PageLayoutAccessory.nib" owner:dpl withNames:NO fromZone:[dpl zone]];
}

return dpl;
}

- orderFrontInspectorPanel:sender
/*
* Creates the inspector panel if it doesn't exist, then orders it front.
*/
{
[[self inspectorPanel] orderFront:self];
return self;
}

/* Setting up the Fax Cover Sheet menu */

#define FAX_NOTE NXLocalString("Notes", NULL, "Fax Cover Sheet item which allows the user to type in any thing he/she wants.")

- setFaxCoverSheetMenu:anObject
/*
* This goes through all the entries in CoverSheet.strings and makes
* an entry in the cover sheet menu for them. This is kind of a kooky
* method, but it makes Draw much more usable as a Fax Cover Sheet
* editor.
*/
{
Menu *fcsMenu;
MenuCell *cell;
const char *key, *value;
NXStringTable *table;
char path[MAXPATHLEN+1];

if (fcsMenu = [anObject target]) {
if ([[NXBundle mainBundle] getPath:path forResource:"CoverSheet" ofType:"strings"]) {
if (table = [[[NXStringTable allocFromZone:[self zone]] init] readFromFile:path]) {
NXHashState state = [table initState];
while ([table nextState:&state key:(void **)&key value:(void **)&value]) {
cell = [fcsMenu addItem:value action:@selector(addLocalizableCoverSheetEntry:) keyEquivalent:0];
[cell setAltTitle:key];
}
[table free];
}
}
[fcsMenu addItem:FAX_NOTE action:@selector(addCoverSheetEntry:) keyEquivalent:0];
}

return self;
}

/* Target/Action methods */

- info:sender
/*
* Brings up the information panel.
*/
{
char buf[20];

if (!infoPanel) {
NXZone *zone = NXCreateChildZone([self zone], vm_page_size, vm_page_size, YES);
NXNameZone(zone, "InfoPanel");
[self loadNibSection:"InfoPanel.nib" owner:self withNames:NO fromZone:zone];
[infoPanel setFrameAutosaveName:"InfoPanel"];
sprintf(buf, "(v%2d)", DrawVersion);
[version setStringValue:buf];
NXMergeZone(zone);
}

[infoPanel orderFront:self];

return self;
}

#define NO_HELP NXLocalString("Couldn't find any help! Sorry ...", NULL, "Message given to the user when the help document could not be found.")

- help:sender
/*
* Loads up the Help draw document.
* Note the use of NXBundle so that the Help document can be localized.
*/
{
DrawDocument *document = nil;
char path[MAXPATHLEN+1];

if ([[NXBundle mainBundle] getPath:path forResource:"Help" ofType:"draw"]) {
if (document = openDocument(path, NO)) {
[document setTemporaryTitle:NXLocalString("Help", NULL, "The title of the help document.")];
[[[document view] window] makeKeyAndOrderFront:self];
}
}

if (!document) NXRunAlertPanel(NULL, NO_HELP, NULL, NULL, NULL);

return self;
}

- new:sender
/*
* Creates a new document--called by pressing New in the Document menu.
*/
{
[DrawDocument new];
return self;
}

- open:sender
/*
* Called by pressing Open... in the Window menu.
*/
{
const char *directory;
const char *const *files;
const char *const drawFileTypes[2] = { "draw", NULL };
OpenPanel *openpanel = [[OpenPanel new] allowMultipleFiles:YES];

directory = [self currentDirectory];
if (directory && (*directory == '/')) [openpanel setDirectory:directory];
if ([openpanel runModalForTypes:drawFileTypes]) {
files = [openpanel filenames];
directory = [openpanel directory];
while (files && *files) {
if (*files) haveOpenedDocument = openFile(directory, *files, YES) || haveOpenedDocument;
files++;
}
}

return self;
}

- saveAll:sender
/*
* Saves all the documents.
*/
{
int count;
Window *window;

count = [windowList count];
while (count--) {
window = [windowList objectAt:count];
[documentInWindow(window) save:sender];
}

return nil;
}

#define UNSAVED_DOCUMENTS NXLocalString("You have unsaved documents.", NULL, "Message given to user when he tries to quit the application without saving all of his documents.")
#define REVIEW_UNSAVED NXLocalString("Review Unsaved", NULL, "Choice (on a button) given to user which allows him to review all his unsaved documents if he quits the application without saving them all first.")
#define QUIT_ANYWAY NXLocalString("Quit Anyway", NULL, "Choice (on a button) given to user which allows him to quit the application even though he has not saved all of his documents first.")
#define QUIT NXLocalString("Quit", NULL, "The operation of exiting the application.")

- terminate:sender cancellable:(BOOL)cancellable
/*
* Makes sure all documents get an opportunity
* to be saved before exiting the program. If we are terminating because
* the user logged out of the workspace (or powered off), then we cannot
* give the user the option of cancelling the quit (that's what the
* cancellable flag is for).
*/
{
int count, choice;
Window *window;
id document;

count = [windowList count];
while (count--) {
window = [windowList objectAt:count];
document = [window delegate];
if ([document respondsTo:@selector(isDirty)] && [document isDirty]) {
if (cancellable) {
choice = NXRunAlertPanel(QUIT, UNSAVED_DOCUMENTS, REVIEW_UNSAVED, QUIT_ANYWAY, CANCEL);
} else {
choice = NXRunAlertPanel(QUIT, UNSAVED_DOCUMENTS, REVIEW_UNSAVED, QUIT_ANYWAY, NULL);
}
if (choice == NX_ALERTOTHER) {
return self;
} else if (choice == NX_ALERTDEFAULT) {
count = [windowList count];
while (count--) {
window = [windowList objectAt:count];
document = [window delegate];
if ([document respondsTo:@selector(windowWillClose:cancellable:)]) {
if (![document windowWillClose:sender cancellable:cancellable] && cancellable) {
return self;
} else {
[document close];
[window close];
}
}
}
}
break;
}
}

return nil;
}

- terminate:sender
/*
* Overridden to give user the opportunity to save unsaved files.
*/
{
if (![self terminate:sender cancellable:YES]) {
return [super terminate:sender];
} else {
return self;
}
}

/*
* Application object delegate methods.
* Since we don't have an application delegate, messages that would
* normally be sent there are sent to the Application object itself instead.
*/

- appDidInit:(Application *)sender
/*
* Makes the tool palette not ever become the key window.
* Check for files to open specified on the command line.
* Initialize the menus.
* If there are no open documents (and we are not being
* launched to service a Services request or otherwise
* being invoked due to interapplication communication),
* then open a blank one.
*/
{
int i;
Panel *toolWindow;

toolWindow = [tools window];
[toolWindow setFrameAutosaveName:[toolWindow title]];
[toolWindow setBecomeKeyOnlyIfNeeded:YES];
[toolWindow setFloatingPanel:YES];
[toolWindow orderFront:self];

if (NXArgc > 1) {
for (i = 1; i < NXArgc; i++) {
haveOpenedDocument = openDocument(NXArgv[i], YES) || haveOpenedDocument;
}
}

if (!haveOpenedDocument && !NXGetDefaultValue([self appName], "NXServiceLaunch")) [self new:self];

if (NXGetDefaultValue([self appName], "Quit")) {
[self activateSelf:YES];
NXPing();
[self terminate:nil];
}

initMenu([NXApp mainMenu]);

return self;
}

- (int)app:sender openFile:(const char *)path type:(const char *)type
/*
* This method is performed whenever a user double-clicks on an icon
* in the Workspace Manager representing a Draw program document.
*/
{
if (type && !strcmp(type, "draw")) {
if (openDocument(path, YES)) {
haveOpenedDocument = YES;
return YES;
}
}

return NO;
}

- (BOOL)appAcceptsAnotherFile:(Application *)sender
/*
* We accept any number of appOpenFile:type: messages.
*/
{
return YES;
}

- app:sender powerOffIn:(int)ms andSave:(int)andSave
/*
* Give the user a chance to save his documents.
*/
{
if (andSave) [self terminate:nil cancellable:NO];
return self;
}

/* Listener/Speaker remote methods */

/*
* Note that anybody can send these messages to a running version
* of Draw, so we protect the ones that can affect the current
* document by only allowing them to happen if the Draw default
* "RemoteControl" is set.
*/

- (int)msgDirectory:(const char **)fullPath ok:(int *)flag
{
*fullPath = [self currentDirectory];
if (*fullPath) {
*flag = YES;
} else {
*fullPath = "";
*flag = NO;
}
return 0;
}

- (int)msgVersion:(const char **)aString ok:(int *)flag
{
char buf[20];

sprintf(buf, "3.0 (v%2d)", DrawVersion);
*aString = NXCopyStringBuffer(buf);
*flag = YES;

return 0;
}

- (int)msgFile:(const char **)fullPath ok:(int *)flag
{
const char *file;

file = [[self currentDocument] filename];
if (file) {
*fullPath = file;
*flag = YES;
} else {
*fullPath = "";
*flag = NO;
}

return 0;
}

- (BOOL)shouldRunPrintPanel:sender
/*
* When NXApp prints, don't bring up the PrintPanel.
*/
{
return NO;
}

- (int)msgPrint:(const char *)fullPath ok:(int *)flag
{
BOOL close;
id document = nil;

if (NXGetDefaultValue("Draw", "RemoteControl")) {
InMsgPrint = YES;
if (document = [findDocument(fullPath) delegate]) {
close = NO;
} else {
document = openDocument(fullPath, NO);
if (document) haveOpenedDocument = YES;
close = YES;
}
if (document && ![[document view] isEmpty]) {
[NXApp setPrintInfo:[document printInfo]];
[[document view] printPSCode:self];
if (close) [[[document view] window] close];
*flag = YES;
} else {
*flag = NO;
}
InMsgPrint = NO;
} else {
*flag = NO;
}

return 0;
}

- (int)msgSelection:(const char **)bytes length:(int *)len asType:(const char *)aType ok:(int *)flag
{
int maxlen;
NXStream *stream;

if (NXGetDefaultValue("Draw", "RemoteControl")) {
if (!strcmp(aType, DrawPboardType)) {
stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
if ([[[self currentDocument] view] copySelectionToStream:stream]) {
NXGetMemoryBuffer(stream, (char **)bytes, len, &maxlen);
*flag = YES;
} else {
*flag = NO;
}
NXClose(stream);
} else if (!strcmp(aType, NXPostScriptPboardType)) {
stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
if ([[[self currentDocument] view] copySelectionAsPSToStream:stream]) {
NXGetMemoryBuffer(stream, (char **)bytes, len, &maxlen);
*flag = YES;
} else {
*flag = NO;
}
NXClose(stream);
} else if (!strcmp(aType, NXTIFFPboardType)) {
stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
if ([[[self currentDocument] view] copySelectionAsTIFFToStream:stream]) {
NXGetMemoryBuffer(stream, (char **)bytes, len, &maxlen);
*flag = YES;
} else {
*flag = NO;
}
NXClose(stream);
} else {
*flag = NO;
}
} else {
*flag = NO;
}

if (!*flag) {
*bytes = "";
*len = 0;
}

return 0;
}

- (int)msgCopyAsType:(const char *)aType ok:(int *)flag
{
if (NXGetDefaultValue("Draw", "RemoteControl")) {
if (!strcmp(aType, NXPostScriptPboardType) ||
!strcmp(aType, NXTIFFPboardType) ||
!strcmp(aType, DrawPboardType)) {
*flag = ([[[self currentDocument] view] copy:self] ? YES : NO);
} else {
*flag = NO;
}
} else {
*flag = NO;
}
return 0;
}

- (int)msgCutAsType:(const char *)aType ok:(int *)flag
{
if (NXGetDefaultValue("Draw", "RemoteControl")) {
if (!strcmp(aType, NXPostScriptPboardType) ||
!strcmp(aType, NXTIFFPboardType) ||
!strcmp(aType, DrawPboardType)) {
*flag = ([[[self currentDocument] view] cut:self] ? YES : NO);
} else {
*flag = NO;
}
} else {
*flag = NO;
}
return 0;
}

- (int)msgPaste:(int *)flag;
{
if (NXGetDefaultValue("Draw", "RemoteControl")) {
*flag = ([[[self currentDocument] view] paste:self] ? YES : NO);
} else {
*flag = NO;
}
return 0;
}

- (int)msgQuit:(int *)flag
{
if (NXGetDefaultValue("Draw", "RemoteControl")) {
*flag = ([self terminate:self cancellable:YES] ? NO : YES);
if (*flag) [NXApp perform:@selector(terminate:) with:nil afterDelay:1 cancelPrevious:YES];
} else {
*flag = NO;
}
return 0;
}

/* Global cursor setting */

- cursor
/*
* This is called by DrawDocument objects who want to set the cursor
* depending on what the currently selected tool is (as well as on whether
* the Control key has been pressed indicating that the select tool is
* temporarily set--see sendEvent:).
*/
{
id theCursor = nil;
if (!cursorPushed) theCursor = [[self currentGraphic] cursor];
return theCursor ? theCursor : NXArrow;
}

- sendEvent:(NXEvent *)event
/*
* We override this because we need to find out when the control key is down
* so we can set the arrow cursor so the user knows she is (temporarily) in
* select mode.
*/
{
if (event && event->type < NX_KITDEFINED) { /* mouse or keyboard event */
if (event->flags & NX_CONTROLMASK) {
if (!cursorPushed && currentGraphic) {
cursorPushed = YES;
[[self currentDocument] resetCursor];
}
} else if (cursorPushed) {
cursorPushed = NO;
[[self currentDocument] resetCursor];
}
}

return [super sendEvent:event];
}

/* Automatic update methods */

- (BOOL)menuItemUpdate:(MenuCell *)menuCell
/*
* Method called by all menu items which send their actions to the
* First Responder. First, if the object which would respond were the
* action sent down the responder chain also responds to the message
* validateCommand:, then it is sent validateCommand: to determine
* whether that command is valid now, otherwise, if there is a responder
* to the message, then it is assumed that the item is valid.
* The method returns YES if the cell has changed its appearance (so that
* the caller (a Menu) knows to redraw it).
*/
{
SEL action;
id responder, target;
BOOL enable;

target = [menuCell target];
enable = [menuCell isEnabled];

if (!target) {
action = [menuCell action];
responder = [self calcTargetForAction:action];
if ([responder respondsTo:@selector(validateCommand:)]) {
enable = [responder validateCommand:menuCell];
} else {
enable = responder ? YES : NO;
}
}

if ([menuCell isEnabled] != enable) {
[menuCell setEnabled:enable];
return YES;
} else {
return NO;
}
}

- (BOOL)validateCommand:(MenuCell *)menuCell
/*
* The only command DrawApp itself controls is saveAll:.
* Save All is enabled only if there are any documents open.
*/
{
SEL action = [menuCell action];

if (action == @selector(saveAll:)) {
return findDocument(NULL) ? YES : NO;
}

return YES;
}

/*
* This is a very funky method and tricks of this sort are not generally
* recommended, but this hack is done so that new Graphic subclasses can
* be added to the program without changing even one line of code (except,
* of course, to implement the subclass itself).
*
* The objective-C method objc_lookUpClass() is used to find the factory object
* corresponding to the name of the icon of the cell sending the setCurrentGraphic:
* message.
*
* Again, this is not recommended procedure, but it illustrates how
* objective-C can be used to make some funky runtime dependent decisions.
*/

- setCurrentGraphic:sender
/*
* The sender's icon is queried. If that name corresponds to the name
* of a class, then that class is set as the currentGraphic. If not,
* then the select tool is put into effect.
*/
{
id cell;
const char *className;

if (cell = [sender selectedCell]) {
if (className = [cell icon]) {
currentGraphic = objc_lookUpClass(className);
} else {
currentGraphic = nil;
}
if (!currentGraphic) [tools selectCellAt:0 :0];
[[self currentDocument] resetCursor];
}

return self;
}

@end