|
|
1.1 root 1:
2: /*
3: GraphDoc.m
4:
5: The GraphDoc represents an open Graph document. GraphDoc receives
6: messages from the user interface objects and uses an Expression object
7: to do calculations and a LineGraph to display the results.
8:
9: The GraphDoc class has one slightly odd external dependency. It requires
10: that the delegate of NXApp be able to provide it with a NXStringTable
11: (via the stringTable method) that it can use to look up strings that
12: are presented to the user. In this application, the delegate of NXApp is
13: always an instance of the GraphApp class.
14:
15: If this were a larger program, the NXStringTable needed by this document
16: class would probably be in its own nib section, which would be loaded once
17: the first time a GraphDoc is created, and then shared among all GraphDocs.
18:
19: You may freely copy, distribute, and reuse the code in this example.
20: NeXT disclaims any warranty of any kind, expressed or implied, as to its
21: fitness for any particular use.
22: */
23:
24: #import "Graph.h"
25:
26: /* declare methods static to this class */
27: @interface GraphDoc(GraphDocPrivate)
28: - _updateGraphVals;
29: - _updateGraph:(BOOL)finalChange;
30: - _updateLinks;
31: - _write:(const char *)filename;
32: - _read:(const char *)filename;
33: - (void)_initVariable:(int)index enable:(BOOL)flag value:(float)val;
34: - _doSaveWithNewName:(BOOL)doNewName retainName:(BOOL)doRetain;
35: - _writeGraphToPasteboard:(Pasteboard *)pb types:(const char *const *)types num:(int)numTypes;
36: - (void)_setSliderCell:(SliderCell *)slider value:(float)val;
37: @end
38:
39: @implementation GraphDoc
40:
41: /* variable names must be between A and H */
42: #define MIN_CONST 'A'
43: #define MAX_CONST 'H'
44:
45: /* default resolution of a new graph */
46: #define DEFAULT_RES 30
47:
48: /* name used for the real document inside a doc wrapper */
49: #define DOC_NAME "/GraphDoc.xygraph"
50:
51: static void writeCellFloat(Cell *obj, NXTypedStream *ts);
52: static void setXRange(Expression *expr, float min, float max);
53: static NXTypedStream *openDocStream(const char *filename, int mode);
54: static int removeFile(const char *file);
55:
56: - init {
57: /* the default initialization is to just open a new document */
58: return [self initFromFile:NULL];
59: }
60:
61: /*
62: * Opens a document. If file is NULL, we open an untitiled document. We also
63: * create a NXDataLinkManager and hook ourselves up as its delegate for doing
64: * Object Links.
65: */
66: - initFromFile:(const char *)file {
67: int rows, cols;
68: int i;
69: TextField *aCell;
70: char realPath[MAXPATHLEN+1];
71:
72: [super init];
73:
74: /* load the UI from the nib section and set attributes not available in IB */
75: [NXApp loadNibSection:"GraphDoc.nib" owner:self withNames:NO fromZone:[self zone]];
76: [variableTexts getNumRows:&rows numCols:&cols];
77: for (i = rows; i--; ) {
78: aCell = [variableTexts cellAt:i :0];
79: [aCell setFloatingPointFormat:YES left:3 right:2];
80: [aCell setEnabled:NO];
81: }
82: [minXText setFloatingPointFormat:YES left:3 right:2];
83: [maxXText setFloatingPointFormat:YES left:3 right:2];
84: [variableSliders setAutosizeCells:YES];
85: [resolutionText setEntryType:NX_POSINTTYPE];
86:
87: /* fake a resize so we get the resolution max up to date */
88: [self windowDidResize:window];
89:
90: /* create an Expression object we will use to evaluate expressions */
91: expr = [[Expression allocFromZone:[self zone]] init];
92:
93: if (file) {
94: /* read existing document */
95: if ([self _read:file]) {
96: name = NXCopyStringBufferFromZone(file, [self zone]);
97: if (realpath(name, realPath))
98: realName = NXCopyStringBufferFromZone(realPath, [self zone]);
99: [window setTitleAsFilename:name];
100: [self _updateGraphVals];
101: linkMgr = [[NXDataLinkManager allocFromZone:[self zone]] initWithDelegate:self fromFile:file];
102: [graph display];
103: } else {
104: [self free]; /* couldn't load file */
105: return nil;
106: }
107: } else {
108: /*
109: * Create a new document. We initialize it with a trivial expression
110: * because that was easier than allowing the state where there is no
111: * current expression.
112: */
113: [graph scaleToFit];
114: [window setTitleAsFilename:[[[NXApp delegate] stringTable] valueForStringKey:"untitled doc"]];
115: [resolutionSlider setIntValue:DEFAULT_RES];
116: [resolutionText setIntValue:DEFAULT_RES];
117: [expr setResolution:DEFAULT_RES];
118: [equation setStringValue:"x"];
119: [expr parse:"x"];
120: setXRange(expr, [minXSlider floatValue], [maxXSlider floatValue]);
121: [self _updateGraphVals];
122: linkMgr = [[NXDataLinkManager allocFromZone:[self zone]] initWithDelegate:self];
123: [graph scaleToFit];
124: [graph display];
125: }
126: [window makeKeyAndOrderFront:self];
127: return self;
128: }
129:
130: - free {
131: [expr free];
132: NXZoneFree([self zone], name);
133: NXZoneFree([self zone], realName);
134: [linkMgr free];
135: return [super free];
136: }
137:
138: - windowDidResize:sender {
139: NXRect frame;
140:
141: /*
142: * Whenever the window changes size, we update the maximum resolution value
143: * to be the width of the graph view.
144: */
145: [graph getFrame:&frame];
146: [resolutionSlider setMaxValue:frame.size.width];
147: return self;
148: }
149:
150: - (const char *)filename {
151: return name;
152: }
153:
154: - (const char *)realFilename {
155: return realName;
156: }
157:
158: /*
159: * This method is called whenever our window becomes the app's main window.
160: * When this happens we get the 3D Panel and set its camera nil, so
161: * the panel reflects the attributes of our window.
162: */
163: - windowDidBecomeMain:sender {
164: [[[NXApp delegate] threeDPanel] setCamera:nil];
165: return self;
166: }
167:
168: #define BUF_MAX 256
169:
170: /*
171: * This method is called when the user has just finished typing in an
172: * expression. This is where we validate that it is a legal expression.
173: * If we dont like the expression, we return YES, which tells the text object
174: * not to accept the user's entry.
175: */
176: - (BOOL)textWillEnd:textObject {
177: const char *var;
178: BOOL parseError;
179: char buffer[BUF_MAX];
180: char *newText;
181: int length;
182: BOOL enableVariables[MAX_CONST - MIN_CONST + 1];
183: int i;
184: EXPEnumState state; /* used to run through the vars of the expression */
185: NXStringTable *stringTable;
186:
187: /* get the text out of the text object where the expression was typed */
188: length = [textObject byteLength] + 1;
189: if (length > BUF_MAX)
190: newText = NXZoneMalloc(NXDefaultMallocZone(), sizeof(char) * length);
191: else
192: newText = buffer;
193: [textObject getSubstring:newText start:0 length:length];
194:
195: if (*newText && (![expr text] || strcmp([expr text], newText))) {
196:
197: /* try parsing the text of the expression */
198: parseError = ![expr parse:newText];
199: if (!parseError) {
200: for (i = 0; i <= MAX_CONST - MIN_CONST; i++)
201: enableVariables[i] = NO;
202:
203: /*
204: * If it parsed successfully, make sure all the variables are single
205: * letters, and are either "x" or between "A" and "H". As we
206: * find suitable variables, we remember then in the enableVariables
207: * array, so we can enable their controls later.
208: */
209: state = [expr beginVariableEnumeration];
210: while (var = [expr nextVariable:state])
211: if (*var && !var[1] && *var >= MIN_CONST && *var <= MAX_CONST)
212: enableVariables[*var - MIN_CONST] = YES;
213: else if (*var != 'x' || var[1])
214: parseError = YES;
215: [expr endVariableEnumeration:state];
216: if (!parseError) {
217: for (i = 0; i <= MAX_CONST - MIN_CONST; i++)
218: [self _initVariable:i enable:enableVariables[i] value:1.0];
219: /* update the range of the x variable */
220: setXRange(expr, [minXSlider floatValue], [maxXSlider floatValue]);
221:
222: /* update the graph object with current results and display it */
223: [self _updateGraphVals];
224: [graph scaleToFit];
225: [graph display];
226: [self _updateLinks];
227: }
228: }
229: } else if (!*newText) {
230: parseError = YES;
231: } else
232: parseError = NO;
233: if (length > BUF_MAX)
234: NXZoneFree(NXDefaultMallocZone(), newText);
235:
236: if (parseError) {
237: stringTable = [[NXApp delegate] stringTable];
238: NXRunAlertPanel([stringTable valueForStringKey:"parse alert title"],
239: [stringTable valueForStringKey:"parse alert message"],
240: [stringTable valueForStringKey:"ok button"],
241: NULL, NULL);
242: }
243: return parseError;
244: }
245:
246: /*
247: * This method is called after the user typed in a new expression. The
248: * expression has already been parsed in our textWillEnd: method.
249: */
250: - equationChanged:sender {
251:
252: /* reselect the equation field so the user can type another */
253: [equation selectText:self];
254: return self;
255: }
256:
257: /* called when either the minx or maxx slider changes */
258: - xRangeSliderChanged:sender {
259: TextFieldCell *text;
260: SliderCell *slider;
261: NXEvent *event;
262:
263: /* update the associated text field */
264: slider = [sender selectedCell];
265: if (slider == maxXSlider)
266: text = maxXText;
267: else if (slider == minXSlider)
268: text = minXText;
269: else
270: text = nil;
271: NX_ASSERT(text, "Funny sender of xRangeSliderChanged: message");
272: [text setFloatValue:[slider floatValue]];
273:
274: /* update the x range in the Expression and display the new graph */
275: setXRange(expr, [minXText floatValue], [maxXText floatValue]);
276: event = [NXApp currentEvent];
277: [self _updateGraph:event->type == NX_LMOUSEUP];
278: return self;
279: }
280:
281: /* called when either the minx or maxx text changes */
282: - xRangeTextChanged:sender {
283: TextFieldCell *text;
284: SliderCell *slider;
285: float val;
286:
287: /*
288: * update the associated slider. If the value typed is outside the current
289: * range of the slider, we extend its range.
290: */
291: text = [sender selectedCell];
292: if (text == maxXText)
293: slider = maxXSlider;
294: else if (text == minXText)
295: slider = minXSlider;
296: else
297: slider = nil;
298: NX_ASSERT(slider, "Funny sender of xRangeTextChanged: message");
299: val = [text floatValue];
300: [self _setSliderCell:slider value:val];
301:
302: /* update the x range in the Expression and display the new graph */
303: setXRange(expr, [minXText floatValue], [maxXText floatValue]);
304: [self _updateGraph:YES];
305: [sender selectCell:text];
306: return self;
307: }
308:
309: /* called when one of the variables' sliders changes */
310: - variableSliderChanged:sender {
311: int index;
312: float val;
313: char varName[2];
314: NXEvent *event;
315:
316: /* update the associated text field */
317: index = [variableSliders selectedRow];
318: val = [[variableSliders selectedCell] floatValue];
319: [[variableTexts cellAt:index :0] setFloatValue:val];
320: varName[0] = MIN_CONST + index;
321: varName[1] = '\0';
322:
323: /* update the variable's value in the Expression and display the new graph */
324: [expr setVar:varName value:val];
325: event = [NXApp currentEvent];
326: [self _updateGraph:event->type == NX_LMOUSEUP];
327: return self;
328: }
329:
330: /* called when one of the variables' text fields changes */
331: - variableTextChanged:sender {
332: int index;
333: float val;
334: char varName[2];
335: TextFieldCell *text;
336: SliderCell *slider;
337:
338: /* update the associated slider */
339: text = [variableTexts selectedCell];
340: index = [variableTexts selectedRow];
341: slider = [variableSliders cellAt:index :0];
342: val = [text floatValue];
343: [self _setSliderCell:slider value:val];
344:
345: /* update the variable's value in the Expression and display the new graph */
346: varName[0] = MIN_CONST + index;
347: varName[1] = '\0';
348: [expr setVar:varName value:val];
349: [self _updateGraph:YES];
350: [sender selectText:self];
351: [sender selectCell:text];
352: return self;
353: }
354:
355: /*
356: * The maximum allowable resolution. The coordinates string of the userpath
357: * that the LineGraph class uses to draw can only have 64K of data (like any
358: * PostScript string). 64K of data is 16K of floats, or 8K coordinate pairs.
359: * But every userpath has to have 4 numbers devoted to the bounding box, so
360: * this reduces the number of allowable points to 8190.
361: *
362: * (In spite of all that nice math, 8190 doesn't seem to work, but 8189 does.
363: * We need to look into this, but for now we don't push the limit.)
364: */
365: #define MAX_RES 8189
366:
367: /* called when either the resolution slider or text fields changes */
368: - resolutionChanged:sender {
369: Cell *senderCell, *otherCell;
370: int iVal;
371: float fVal;
372: NXEvent *event;
373:
374: if ([[sender cellAt:0 :0] isKindOfClassNamed:"SliderCell"]) {
375: senderCell = resolutionSlider;
376: otherCell = resolutionText;
377: } else {
378: senderCell = resolutionText;
379: otherCell = resolutionSlider;
380: }
381:
382: fVal = [senderCell floatValue];
383: iVal = rint(fVal);
384: if (iVal > MAX_RES)
385: iVal = MAX_RES;
386: [otherCell setIntValue:iVal];
387:
388: /* update the Expression's resolution and display the new graph */
389: [expr setResolution:iVal];
390: event = [NXApp currentEvent];
391: [self _updateGraph:event->type != NX_LMOUSEDRAGGED];
392: if (senderCell == resolutionText)
393: [sender selectText:self];
394: return self;
395: }
396:
397: /* called when the zoom in button is pressed */
398: - zoomIn:sender {
399: [autoScale setIntValue:0];
400: [graph zoom:2.0];
401: [graph display];
402: [self _updateLinks];
403: return self;
404: }
405:
406: /* called when the zoom out button is pressed */
407: - zoomOut:sender {
408: [autoScale setIntValue:0];
409: [graph zoom:0.5];
410: [graph display];
411: [self _updateLinks];
412: return self;
413: }
414:
415: /* called when the auto scale switch changes */
416: - autoScale:sender {
417: if ([autoScale intValue]) {
418: [graph scaleToFit]; /* it got turned on, get scaled to fit */
419: [graph display];
420: [self _updateLinks];
421: }
422: return self;
423: }
424:
425: /*
426: * Copies the current view of the graph into the Pasteboard as PostScript. It
427: * also writes a DataLink to the Pasteboard which can be used to create an
428: * Object Link to the graph. Since there is no notion of user selection
429: * within a graph, its very easy for us to generate NXSelection objects - we
430: * just use the standard Selection meaning "Select All". This is sent from the
431: * Copy Graph menu item.
432: */
433: - copyGraph:sender {
434: Pasteboard *pb;
435: const char *types[2];
436: NXDataLink *link;
437:
438: pb = [Pasteboard new];
439: types[0] = NXPostScriptPboardType;
440: types[1] = NXDataLinkPboardType;
441: [self _writeGraphToPasteboard:pb types:types num:2];
442: link = [[NXDataLink alloc] initLinkedToSourceSelection:[NXSelection allSelection] managedBy:linkMgr supportingTypes:&NXPostScriptPboardType count:1];
443: [link writeToPasteboard:pb];
444: [link free];
445: return self;
446: }
447:
448: /*
449: * Does the real work of copying the graph as PostScript. This code is used
450: * for copy/paste and Object Links support.
451: *
452: * Types must contain NXPostScriptPboardType.
453: */
454: - _writeGraphToPasteboard:(Pasteboard *)pb types:(const char *const *)types num:(int)numTypes {
455: NXStream *st;
456: char *data;
457: int dataLen, maxDataLen;
458:
459: /* Open a stream on memory where we will collect the PostScript */
460: st = NXOpenMemory(NULL, 0, NX_WRITEONLY);
461:
462: /* Tell the Pasteboard we're going to copy PostScript */
463: [pb declareTypes:types num:numTypes owner:nil];
464:
465: /* writes the PostScript for the whole graph as EPS into the stream */
466: [graph copyPSCodeInside:NULL to:st];
467:
468: /* get the buffered up PostScript out of the stream */
469: NXGetMemoryBuffer(st, &data, &dataLen, &maxDataLen);
470:
471: /* put the buffer in the Pasteboard, free the stream (and the buffer) */
472: [pb writeType:NXPostScriptPboardType data:data length:dataLen];
473: NXCloseMemory(st, NX_FREEBUFFER);
474: return self;
475: }
476:
477: /*** Object Links support - methods called by the DataLinkManager ***/
478:
479: /* called by the DataLinkManager when new data is needed for a link */
480: - copyToPasteboard:(Pasteboard *)pboard at:(NXSelection *)selection cheapCopyAllowed:(BOOL)flag {
481: NX_ASSERT([selection isEqual:[NXSelection allSelection]] || [selection isEqual:[NXSelection currentSelection]], "Funny selection passed to copyToPasteboard:at:");
482: [self _writeGraphToPasteboard:pboard types:&NXPostScriptPboardType num:1];
483: return self;
484: }
485:
486: /* returns the window for the given selection */
487: - windowForSelection:(NXSelection *)selection {
488: return window; /* all our sels are always in our one window */
489: }
490:
491: /*
492: * We support continuously updating links by tracking changes to links to
493: * us from open documents. This is easy for Graph because all links can
494: * only be to the whole graph, so any change in the graph is a change relevant
495: * to any link.
496: */
497: - (BOOL)dataLinkManagerTracksLinksIndividually:(NXDataLinkManager *)sender {
498: return YES;
499: }
500:
501: /*
502: * Sent when we should start tracking a link. We just keep all links we're
503: * tracking in a list, and send them a message whenever the graph is changed.
504: */
505: - dataLinkManager:(NXDataLinkManager *)sender startTrackingLink:(NXDataLink *)link {
506: if (!linksTracked)
507: linksTracked = [[List allocFromZone:[self zone]] init];
508: [linksTracked addObject:link];
509: return self;
510: }
511:
512: /* Sent when we can forget a links we're tracking */
513: - dataLinkManager:(NXDataLinkManager *)sender stopTrackingLink:(NXDataLink *)link {
514: [linksTracked removeObject:link];
515: if ([linksTracked count] == 0) {
516: [linksTracked free];
517: linksTracked = nil;
518: }
519: return self;
520: }
521:
522: /*
523: * Called by other methods of GraphDoc whenever we make a change to the
524: * document. We tell the DataLinkManager about the change, and also notify
525: * any links we are tracking.
526: */
527: - _updateLinks {
528: [linkMgr documentEdited];
529: [linksTracked makeObjectsPerform:@selector(sourceEdited)];
530: [window setDocEdited:YES];
531: return self;
532: }
533:
534: /* called when Save, Save As, or Save To is picked from the menu */
535: - save:sender { return [self _doSaveWithNewName:NO retainName:YES]; }
536: - saveAs:sender { return [self _doSaveWithNewName:YES retainName:YES]; }
537: - saveTo:sender { return [self _doSaveWithNewName:YES retainName:NO]; }
538:
539: /*
540: * All varieties of save go through this routine. It covers all the cases
541: * of running the Save Panel and retaining the name chosen.
542: */
543: - _doSaveWithNewName:(BOOL)doNewName retainName:(BOOL)doRetain {
544: SavePanel *savePanel;
545: const char *saveName; /* filename to save into */
546: NXZone *zone;
547: BOOL previouslySaved = (name != NULL);
548: char realPath[MAXPATHLEN+1];
549:
550: /*
551: * If the file is untitled or we are saving under a different name,
552: * run the save panel to get a new name.
553: */
554: zone = [self zone];
555: if (!name || doNewName) {
556: savePanel = [SavePanel new];
557: [savePanel setRequiredFileType:"xygraph"];
558: if ([savePanel runModalForDirectory:NULL file:name]) {
559: /* if we want to keep this name, replace any old name */
560: if (doRetain) {
561: NXZoneFree(zone, name);
562: NXZoneFree(zone, realName);
563: realName = NULL;
564: name = NXCopyStringBufferFromZone([savePanel filename], zone);
565: }
566: saveName = [savePanel filename];
567: } else
568: return nil; /* user canceled */
569: } else
570: /* if we didn't run the Save Panel, save using the existing name */
571: saveName = name;
572: [self _write:saveName];
573: if (!doRetain)
574: [linkMgr documentSavedTo:saveName];
575: else if (!previouslySaved || doNewName)
576: [linkMgr documentSavedAs:saveName];
577: else
578: [linkMgr documentSaved];
579: if (doRetain) {
580: [window setDocEdited:NO];
581: [window setTitleAsFilename:name];
582: if (!realName && realpath(name, realPath)) {
583: realName = NXCopyStringBufferFromZone(realPath, [self zone]);
584: }
585: }
586: return self;
587: }
588:
589: - revertToSaved:sender {
590: int response;
591: NXStringTable *stringTable;
592: const char *shortName;
593: GraphDoc *newDoc;
594:
595: if ([window isDocEdited] && name) {
596: stringTable = [[NXApp delegate] stringTable];
597: shortName = strrchr(name, '/') + 1;
598: response = NXRunAlertPanel([stringTable valueForStringKey:"revert alert title"],
599: [stringTable valueForStringKey:"revert alert message"],
600: [stringTable valueForStringKey:"revert button"],
601: [stringTable valueForStringKey:"cancel button"],
602: NULL, shortName);
603: if (response == NX_ALERTDEFAULT) {
604: [window setDelegate:nil];
605: [window close];
606: [NXApp delayedFree:self];
607: [linkMgr documentClosed];
608: [linkMgr free];
609: linkMgr = nil;
610: newDoc = [[GraphDoc allocFromZone:[self zone]] initFromFile:name];
611: if (!newDoc) {
612: NXRunAlertPanel(
613: [stringTable valueForStringKey:"open alert title"],
614: [stringTable valueForStringKey:"open alert message"],
615: [stringTable valueForStringKey:"ok button"],
616: NULL, NULL, name);
617: }
618: }
619: }
620: return self;
621: }
622:
623: /* switches the colors of the graph and its background. */
624: - invertColors:sender {
625: float bgGray;
626:
627: bgGray = [graph backgroundGray];
628: [graph setBackgroundGray:[graph lineGray]];
629: [graph setLineGray:bgGray];
630: [graph display]; /* draw the new graph */
631: return self;
632: }
633:
634: /* Called when the window is closing. We free the GraphDoc. */
635: - windowWillClose:sender {
636: int response;
637: NXStringTable *stringTable;
638: const char *shortName;
639:
640: if ([window isDocEdited]) {
641: stringTable = [[NXApp delegate] stringTable];
642: if (name) {
643: shortName = strrchr(name, '/') + 1;
644: } else {
645: shortName = [stringTable valueForStringKey:"untitled doc"];
646: }
647: response = NXRunAlertPanel([stringTable valueForStringKey:"close alert title"],
648: [stringTable valueForStringKey:"close alert message"],
649: [stringTable valueForStringKey:"save button"],
650: [stringTable valueForStringKey:"dont save button"],
651: [stringTable valueForStringKey:"cancel button"],
652: shortName);
653: if (response != NX_ALERTDEFAULT && response != NX_ALERTALTERNATE) {
654: return nil;
655: } else {
656: if (response == NX_ALERTDEFAULT && ![self save:sender])
657: return nil;
658: }
659: }
660: [NXApp delayedFree:self];
661: [linkMgr documentClosed];
662: return self; /* says its OK to close */
663: }
664:
665: /* Called whenever something changes to update the view of the graph. */
666: - _updateGraph:(BOOL)finalChange {
667: [self _updateGraphVals]; /* update the values that we graph */
668: if ([autoScale intValue])
669: [graph scaleToFit];
670: [graph display]; /* draw the new graph */
671: if (finalChange)
672: [self _updateLinks];
673: return self;
674: }
675:
676: /* Called whenever something changes to update the values that we graph. */
677: - _updateGraphVals {
678: float *xVals, *yVals;
679: int numXVals, numYVals;
680: float minX, minY, maxX, maxY;
681:
682: /* extract the x values from the Expression */
683: [expr varVector:"x" vector:&xVals numVals:&numXVals];
684: [expr var:"x" min:&minX max:&maxX];
685:
686: /* extract the y values from the Expression. This may cause a recalc. */
687: [expr resultsVector:&yVals numVals:&numYVals];
688: [expr resultsMin:&minY max:&maxY];
689:
690: /* set the points of the graph */
691: [graph setPoints:[expr resolution] x:xVals y:yVals
692: minX:minX minY:minY maxX:maxX maxY:maxY];
693: return self;
694: }
695:
696: /*
697: * Writes a document to the given file using typedstreams. We're very careful
698: * about catching exceptions here so we can tell the user if his file didn't
699: * get written out successfully.
700: */
701: - _write:(const char *)filename {
702: NXTypedStream *ts;
703: NXRect graphViewRect;
704: const char *stringVal;
705: int intVal;
706: float floatVal;
707: char charVal;
708: int rows, cols;
709: int numVars;
710: int i;
711: NXStringTable *stringTable;
712: volatile BOOL hadAnError = NO;
713: char buffer[MAXPATHLEN+1];
714:
715: /* cons up the name of the backup file and remove it */
716: strcpy(buffer, filename);
717: strcat(buffer, "~");
718: removeFile(buffer);
719:
720: /* move the existing file to the backup */
721: rename(filename, buffer);
722:
723: /* create the new document as a file package (doc wrapper) */
724: strcpy(buffer, filename);
725: strcat(buffer, DOC_NAME);
726: mkdir(filename, 0777);
727: ts = NXOpenTypedStreamForFile(buffer, NX_WRITEONLY);
728: if (ts) {
729: NX_DURING
730: intVal = 1; /* version of this write's data */
731: NXWriteType(ts, "i", &intVal);
732: stringVal = [expr text];
733: NXWriteType(ts, "*", &stringVal);
734: [graph getBounds:&graphViewRect];
735: NXWriteRect(ts, &graphViewRect);
736: [variableSliders getNumRows:&rows numCols:&cols];
737: numVars = 0;
738: for (i = 0; i < rows; i++) {
739: if ([[variableSliders cellAt:i :0] isEnabled])
740: numVars++;
741: }
742: NXWriteType(ts, "i", &numVars);
743: for (i = 0; i < rows; i++)
744: if ([[variableSliders cellAt:i :0] isEnabled]) {
745: charVal = MIN_CONST + i;
746: NXWriteType(ts, "c", &charVal);
747: writeCellFloat([variableSliders cellAt:i :0], ts);
748: }
749: intVal = [expr resolution];
750: NXWriteType(ts, "i", &intVal);
751: writeCellFloat(minXSlider, ts);
752: writeCellFloat(maxXSlider, ts);
753: floatVal = [graph backgroundGray];
754: NXWriteType(ts, "f", &floatVal);
755: floatVal = [graph lineGray];
756: NXWriteType(ts, "f", &floatVal);
757: intVal = [autoScale intValue];
758: NXWriteType(ts, "i", &intVal);
759: NXCloseTypedStream(ts);
760: NX_HANDLER
761: hadAnError = YES;
762: NX_DURING
763: NXCloseTypedStream(ts);
764: NX_HANDLER
765: /* ignore any error at this point */
766: NX_ENDHANDLER
767: NX_ENDHANDLER
768: } else
769: hadAnError = YES;
770: if (hadAnError) {
771: stringTable = [[NXApp delegate] stringTable];
772: NXRunAlertPanel([stringTable valueForStringKey:"save alert title"],
773: [stringTable valueForStringKey:"save alert message"],
774: [stringTable valueForStringKey:"ok button"],
775: NULL, NULL, filename);
776: return nil;
777: } else
778: return self;
779: }
780:
781: /* reads a document from the given file using typedstreams */
782: - _read:(const char *)filename {
783: NXTypedStream *ts;
784: NXRect graphViewRect;
785: char *stringVal;
786: int i;
787: char varName;
788: float floatVal1, floatVal2;
789: int intVal;
790: int numVars;
791: BOOL parseError;
792:
793: ts = openDocStream(filename, NX_READONLY);
794: if (ts) {
795: NX_DURING
796: NXReadType(ts, "i", &intVal); /* read file version */
797: NX_ASSERT(intVal == 1, "Unknown file version in -read");
798: NXReadType(ts, "*", &stringVal);
799: [equation setStringValue:stringVal];
800: parseError = ![expr parse:stringVal];
801: free(stringVal);
802: NX_ASSERT(!parseError, "Bad expression stored in data file");
803: if (parseError)
804: NX_VALRETURN(nil);
805: NXReadRect(ts, &graphViewRect);
806: [graph setDrawSize:graphViewRect.size.width
807: :graphViewRect.size.height];
808: [graph setDrawOrigin:graphViewRect.origin.x
809: :graphViewRect.origin.y];
810: NXReadType(ts, "i", &numVars);
811: for (i = 0; i < numVars; i++) {
812: NXReadType(ts, "c", &varName);
813: NXReadType(ts, "f", &floatVal1);
814: [self _initVariable:varName - MIN_CONST enable:YES
815: value:floatVal1];
816: }
817: NXReadType(ts, "i", &intVal); /* read resolution */
818: [resolutionText setIntValue:intVal];
819: [self _setSliderCell:resolutionSlider value:intVal];
820: [expr setResolution:intVal];
821: NXReadType(ts, "f", &floatVal1); /* read minX */
822: NXReadType(ts, "f", &floatVal2); /* read maxX */
823: [minXText setFloatValue:floatVal1];
824: [self _setSliderCell:minXSlider value:floatVal1];
825: [maxXText setFloatValue:floatVal2];
826: [self _setSliderCell:maxXSlider value:floatVal2];
827: setXRange(expr, floatVal1, floatVal2);
828: NXReadType(ts, "f", &floatVal1); /* read background gray */
829: [graph setBackgroundGray:floatVal1];
830: NXReadType(ts, "f", &floatVal1); /* read line gray */
831: [graph setLineGray:floatVal1];
832: NXReadType(ts, "i", &intVal); /* read auto scale */
833: [autoScale setIntValue:intVal];
834: NXCloseTypedStream(ts);
835: /* must use NX_VALRETURN to return from inside a NX_DURING */
836: NX_VALRETURN(self);
837: NX_HANDLER
838: NX_DURING
839: NXCloseTypedStream(ts);
840: NX_HANDLER
841: /* ignore any error at this point */
842: NX_ENDHANDLER
843: return nil;
844: NX_ENDHANDLER
845: } else
846: return nil;
847: }
848:
849: /*
850: * Inits a variable to either be on or off. Used when we discover a new
851: * set of variables after a parse, or to set up the variables from a document
852: * that we read from a file.
853: */
854: - (void)_initVariable:(int)index enable:(BOOL)flag value:(float)val {
855: TextFieldCell *text;
856: SliderCell *slider;
857: char varName[2];
858:
859: /* set the initial value in the Expression */
860: if (flag) {
861: varName[0] = MIN_CONST + index;
862: varName[1] = '\0';
863: [expr setVar:varName value:val];
864: }
865:
866: /* if the enabled status is changing... */
867: if (flag != [[variableSliders cellAt:index :0] isEnabled])
868: if (flag) {
869:
870: /* enable all controls associated with the variable */
871: [[variableLabels cellAt:index :0] setTextGray:NX_BLACK];
872: slider = [variableSliders cellAt:index :0];
873: [slider setEnabled:YES];
874: [self _setSliderCell:slider value:val];
875: text = [variableTexts cellAt:index :0];
876: [text setEnabled:YES];
877: [text setFloatValue:val];
878: } else if (!flag) {
879:
880: /* disable all controls associated with the variable */
881: [[variableLabels cellAt:index :0] setTextGray:NX_DKGRAY];
882: slider = [variableSliders cellAt:index :0];
883: [slider setFloatValue:[slider minValue]];
884: [slider setEnabled:NO];
885: text = [variableTexts cellAt:index :0];
886: [text setStringValue:NULL];
887: [text setEnabled:NO];
888: }
889: }
890:
891: /* sets a slider to a value and adjust min/max */
892: - (void)_setSliderCell:(SliderCell *)slider value:(float)val {
893: if (val < [slider minValue]) {
894: [window disableDisplay]; /* so slider won't flash to new location */
895: [slider setMinValue:val];
896: [window reenableDisplay];
897: } else if (val > [slider maxValue]) {
898: [window disableDisplay]; /* so slider won't flash to new location */
899: [slider setMaxValue:val];
900: [window reenableDisplay];
901: }
902: [slider setFloatValue:val];
903: }
904:
905: @end
906:
907: /* little utility proc to write out the float value of a control */
908: static void writeCellFloat(Cell *obj, NXTypedStream *ts) {
909: float val;
910:
911: val = [obj floatValue];
912: NXWriteType(ts, "f", &val);
913: }
914:
915:
916: /*
917: * A little utility proc to set the range of the x variable in the Expression.
918: * It ensures that the min value isn't greater than the max value.
919: */
920: static void setXRange(Expression *expr, float min, float max) {
921: [expr setVar:"x" min:MIN(min, max) max:MAX(min, max)];
922: }
923:
924: /* Opens a stream on the document regardless of whether its a doc wrapper. */
925: static NXTypedStream *openDocStream(const char *filename, int mode) {
926: NXTypedStream *ts = NULL;
927: struct stat statInfo;
928: char buffer[MAXPATHLEN+1];
929:
930: if (stat(filename, &statInfo) == 0) {
931: if (statInfo.st_mode & S_IFDIR) {
932: strcpy(buffer, filename);
933: strcat(buffer, DOC_NAME);
934: ts = NXOpenTypedStreamForFile(buffer, mode);
935: } else {
936: ts = NXOpenTypedStreamForFile(filename, mode);
937: }
938: }
939: return ts;
940: }
941:
942: /* removes a directory, removing anything inside it. Does not recurse */
943: static int removeFile(const char *file) {
944: DIR *dirp;
945: struct stat st;
946: struct direct *dp;
947: char *leaf = NULL;
948: char path[MAXPATHLEN+1];
949:
950: if (!stat(file, &st)) {
951: if ((st.st_mode & S_IFMT) == S_IFDIR) {
952: dirp = opendir(file);
953: for (dp = readdir(dirp); dp != NULL; dp = readdir(dirp)) {
954: if (strcmp(dp->d_name, ".") && strcmp(dp->d_name, "..")) {
955: if (!leaf) {
956: strcpy(path, file);
957: strcat(path, "/");
958: leaf = path + strlen(path);
959: }
960: strcpy(leaf, dp->d_name);
961: if (unlink(path)) {
962: closedir(dirp);
963: return -1;
964: }
965: }
966: }
967: return rmdir(file);
968: } else {
969: return unlink(file);
970: }
971: }
972:
973: return -1;
974: }
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.