home *** CD-ROM | disk | FTP | other *** search
/ NeXTSTEP 3.0 / NeXTSTEP3.0.iso / NextDeveloper / Examples / AppKit / Graph / GraphDoc.m < prev    next >
Encoding:
Text File  |  1992-06-24  |  28.0 KB  |  921 lines

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