PageRenderTime 61ms CodeModel.GetById 41ms app.highlight 10ms RepoModel.GetById 1ms app.codeStats 0ms

/AppKit/CPDocument.j

http://github.com/cacaodev/cappuccino
Unknown | 920 lines | 757 code | 163 blank | 0 comment | 0 complexity | 9c94d25031367a70c39f529770468472 MD5 | raw file
  1/*
  2 * CPDocument.j
  3 * AppKit
  4 *
  5 * Created by Francisco Tolmasky.
  6 * Copyright 2008, 280 North, Inc.
  7 *
  8 * This library is free software; you can redistribute it and/or
  9 * modify it under the terms of the GNU Lesser General Public
 10 * License as published by the Free Software Foundation; either
 11 * version 2.1 of the License, or (at your option) any later version.
 12 *
 13 * This library is distributed in the hope that it will be useful,
 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 16 * Lesser General Public License for more details.
 17 *
 18 * You should have received a copy of the GNU Lesser General Public
 19 * License along with this library; if not, write to the Free Software
 20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 21 */
 22
 23@import <Foundation/CPString.j>
 24@import <Foundation/CPArray.j>
 25
 26@import "CPAlert.j"
 27@import "CPResponder.j"
 28@import "CPSavePanel.j"
 29@import "CPViewController.j"
 30@import "CPWindowController.j"
 31
 32@class CPDocumentController
 33
 34@global CPApp
 35
 36
 37/*
 38    @global
 39    @group CPSaveOperationType
 40*/
 41CPSaveOperation             = 0;
 42/*
 43    @global
 44    @group CPSaveOperationType
 45*/
 46CPSaveAsOperation           = 1;
 47/*
 48    @global
 49    @group CPSaveOperationType
 50*/
 51CPSaveToOperation           = 2;
 52/*
 53    @global
 54    @group CPSaveOperationType
 55*/
 56CPAutosaveOperation         = 3;
 57
 58/*
 59    @global
 60    @group CPDocumentChangeType
 61*/
 62CPChangeDone                = 0;
 63/*
 64    @global
 65    @group CPDocumentChangeType
 66*/
 67CPChangeUndone              = 1;
 68/*
 69    @global
 70    @group CPDocumentChangeType
 71*/
 72CPChangeCleared             = 2;
 73/*
 74    @global
 75    @group CPDocumentChangeType
 76*/
 77CPChangeReadOtherContents   = 3;
 78/*
 79    @global
 80    @group CPDocumentChangeType
 81*/
 82CPChangeAutosaved           = 4;
 83
 84CPDocumentWillSaveNotification      = @"CPDocumentWillSaveNotification";
 85CPDocumentDidSaveNotification       = @"CPDocumentDidSaveNotification";
 86CPDocumentDidFailToSaveNotification = @"CPDocumentDidFailToSaveNotification";
 87
 88var CPDocumentUntitledCount = 0;
 89
 90/*!
 91    @ingroup appkit
 92    @class CPDocument
 93
 94    CPDocument is used to represent a document/file in a Cappuccino application.
 95    In a document-based application, generally multiple documents are open simultaneously
 96    (multiple text documents, slide presentations, spreadsheets, etc.), and multiple
 97    CPDocuments should be used to represent this.
 98*/
 99@implementation CPDocument : CPResponder
100{
101    CPWindow            _window; // For outlet purposes.
102    CPView              _view; // For outlet purposes
103    CPDictionary        _viewControllersForWindowControllers;
104
105    CPURL               _fileURL;
106    CPString            _fileType;
107    CPArray             _windowControllers;
108    unsigned            _untitledDocumentIndex;
109
110    BOOL                _hasUndoManager;
111    CPUndoManager       _undoManager;
112
113    int                 _changeCount;
114
115    CPURLConnection     _readConnection;
116    CPURLRequest        _writeRequest;
117
118    CPAlert             _canCloseAlert;
119}
120
121/*!
122    Initializes an empty document.
123    @return the initialized document
124*/
125- (id)init
126{
127    self = [super init];
128
129    if (self)
130    {
131        _windowControllers = [];
132        _viewControllersForWindowControllers = @{};
133
134        _hasUndoManager = YES;
135        _changeCount = 0;
136
137        [self setNextResponder:CPApp];
138    }
139
140    return self;
141}
142
143/*!
144    Initializes the document with a specific data type.
145    @param aType the type of document to initialize
146    @param anError not used
147    @return the initialized document
148*/
149- (id)initWithType:(CPString)aType error:(/*{*/CPError/*}*/)anError
150{
151    self = [self init];
152
153    if (self)
154        [self setFileType:aType];
155
156    return self;
157}
158
159/*!
160    Initializes a document of a specific type located at a URL. Notifies
161    the provided delegate after initialization.
162    @param anAbsoluteURL the url of the document content
163    @param aType the type of document located at the URL
164    @param aDelegate the delegate to notify
165    @param aDidReadSelector the selector used to notify the delegate
166    @param aContextInfo context information passed to the delegate
167    after initialization
168    @return the initialized document
169*/
170- (id)initWithContentsOfURL:(CPURL)anAbsoluteURL ofType:(CPString)aType delegate:(id)aDelegate didReadSelector:(SEL)aDidReadSelector contextInfo:(id)aContextInfo
171{
172    self = [self init];
173
174    if (self)
175    {
176        [self setFileURL:anAbsoluteURL];
177        [self setFileType:aType];
178
179        [self readFromURL:anAbsoluteURL ofType:aType delegate:aDelegate didReadSelector:aDidReadSelector contextInfo:aContextInfo];
180    }
181
182    return self;
183}
184
185/*!
186    Initializes the document from a URL.
187    @param anAbsoluteURL the document location
188    @param absoluteContentsURL the location of the document's contents
189    @param aType the type of the contents
190    @param aDelegate this object will receive a callback after the document's contents are loaded
191    @param aDidReadSelector the message selector that will be sent to \c aDelegate
192    @param aContextInfo passed as the argument to the message sent to the \c aDelegate
193    @return the initialized document
194*/
195- (id)initForURL:(CPURL)anAbsoluteURL withContentsOfURL:(CPURL)absoluteContentsURL ofType:(CPString)aType delegate:(id)aDelegate didReadSelector:(SEL)aDidReadSelector contextInfo:(id)aContextInfo
196{
197    self = [self init];
198
199    if (self)
200    {
201        [self setFileURL:anAbsoluteURL];
202        [self setFileType:aType];
203
204        [self readFromURL:absoluteContentsURL ofType:aType delegate:aDelegate didReadSelector:aDidReadSelector contextInfo:aContextInfo];
205    }
206
207    return self;
208}
209
210/*!
211    Returns the receiver's data in a specified type. The default implementation just
212    throws an exception.
213    @param aType the format of the data
214    @param anError not used
215    @throws CPUnsupportedMethodException if this method hasn't been overridden by the subclass
216    @return the document data
217*/
218- (CPData)dataOfType:(CPString)aType error:(/*{*/CPError/*}*/)anError
219{
220    [CPException raise:CPUnsupportedMethodException
221                reason:"dataOfType:error: must be overridden by the document subclass."];
222}
223
224/*!
225    Sets the content of the document by reading the provided
226    data. The default implementation just throws an exception.
227    @param aData the document's data
228    @param aType the document type
229    @param anError not used
230    @throws CPUnsupportedMethodException if this method hasn't been
231    overridden by the subclass
232*/
233- (void)readFromData:(CPData)aData ofType:(CPString)aType error:(CPError)anError
234{
235    [CPException raise:CPUnsupportedMethodException
236                reason:"readFromData:ofType: must be overridden by the document subclass."];
237}
238
239- (void)viewControllerWillLoadCib:(CPViewController)aViewController
240{
241}
242
243- (void)viewControllerDidLoadCib:(CPViewController)aViewController
244{
245}
246
247// Creating and managing window controllers
248/*!
249    Creates the window controller for this document.
250*/
251- (void)makeWindowControllers
252{
253    [self makeViewAndWindowControllers];
254}
255
256- (void)makeViewAndWindowControllers
257{
258    var viewCibName = [self viewCibName],
259        windowCibName = [self windowCibName],
260        viewController = nil,
261        windowController = nil;
262
263    // Create our view controller if we have a cib for it.
264    if ([viewCibName length])
265        viewController = [[CPViewController alloc] initWithCibName:viewCibName bundle:nil owner:self];
266
267    // From a cib if we have one.
268    if ([windowCibName length])
269        windowController = [[CPWindowController alloc] initWithWindowCibName:windowCibName owner:self];
270
271    // If not you get a standard window capable of displaying multiple documents and view
272    else if (viewController)
273    {
274        var view = [viewController view],
275            viewFrame = [view frame];
276
277        viewFrame.origin = CGPointMake(50, 50);
278
279        var theWindow = [[CPWindow alloc] initWithContentRect:viewFrame styleMask:CPTitledWindowMask | CPClosableWindowMask | CPMiniaturizableWindowMask | CPResizableWindowMask];
280
281        windowController = [[CPWindowController alloc] initWithWindow:theWindow];
282    }
283
284    if (windowController && viewController)
285        [windowController setSupportsMultipleDocuments:YES];
286
287    if (windowController)
288        [self addWindowController:windowController];
289
290    if (viewController)
291        [self addViewController:viewController forWindowController:windowController];
292}
293
294/*!
295    Returns the document's window controllers
296*/
297- (CPArray)windowControllers
298{
299    return _windowControllers;
300}
301
302/*!
303    Add a controller to the document's list of controllers. This should
304    be called after making a new window controller.
305    @param aWindowController the controller to add
306*/
307- (void)addWindowController:(CPWindowController)aWindowController
308{
309    [_windowControllers addObject:aWindowController];
310
311    if ([aWindowController document] !== self)
312        [aWindowController setDocument:self];
313}
314
315/*!
316    Remove a controller to the document's list of controllers. This should
317    be called after closing the controller's window.
318    @param aWindowController the controller to remove
319*/
320- (void)removeWindowController:(CPWindowController)aWindowController
321{
322    if (aWindowController)
323        [_windowControllers removeObject:aWindowController];
324
325    if ([aWindowController document] === self)
326        [aWindowController setDocument:nil];
327}
328
329- (CPView)view
330{
331    return _view;
332}
333
334- (CPArray)viewControllers
335{
336    return [_viewControllersForWindowControllers allValues];
337}
338
339- (void)addViewController:(CPViewController)aViewController forWindowController:(CPWindowController)aWindowController
340{
341    // FIXME: exception if we don't own the window controller?
342    [_viewControllersForWindowControllers setObject:aViewController forKey:[aWindowController UID]];
343
344    if ([aWindowController document] === self)
345        [aWindowController setViewController:aViewController];
346}
347
348- (void)removeViewController:(CPViewController)aViewController
349{
350    [_viewControllersForWindowControllers removeObject:aViewController];
351}
352
353- (CPViewController)viewControllerForWindowController:(CPWindowController)aWindowController
354{
355    return [_viewControllersForWindowControllers objectForKey:[aWindowController UID]];
356}
357
358// Managing Document Windows
359/*!
360    Shows all the document's windows.
361*/
362- (void)showWindows
363{
364    [_windowControllers makeObjectsPerformSelector:@selector(setDocument:) withObject:self];
365    [_windowControllers makeObjectsPerformSelector:@selector(showWindow:) withObject:self];
366}
367
368/*!
369    Returns the name of the document as displayed in the title bar.
370*/
371- (CPString)displayName
372{
373    if (_fileURL)
374        return [_fileURL lastPathComponent];
375
376    if (!_untitledDocumentIndex)
377        _untitledDocumentIndex = ++CPDocumentUntitledCount;
378
379    if (_untitledDocumentIndex == 1)
380       return @"Untitled";
381
382    return @"Untitled " + _untitledDocumentIndex;
383}
384
385- (CPString)viewCibName
386{
387    return nil;
388}
389
390/*!
391    Returns the document's Cib name
392*/
393- (CPString)windowCibName
394{
395    return nil;
396}
397
398/*!
399    Called after \c aWindowController loads the document's Nib file.
400    @param aWindowController the controller that loaded the Nib file
401*/
402- (void)windowControllerDidLoadCib:(CPWindowController)aWindowController
403{
404}
405
406/*!
407    Called before \c aWindowController will load the document's Nib file.
408    @param aWindowController the controller that will load the Nib file
409*/
410- (void)windowControllerWillLoadCib:(CPWindowController)aWindowController
411{
412}
413
414// Reading from and Writing to URLs
415/*!
416    Set the document's data from a URL. Notifies the provided delegate afterwards.
417    @param anAbsoluteURL the URL to the document's content
418    @param aType the document type
419    @param aDelegate delegate to notify after reading the data
420    @param aDidReadSelector message that will be sent to the delegate
421    @param aContextInfo context information that gets sent to the delegate
422*/
423- (void)readFromURL:(CPURL)anAbsoluteURL ofType:(CPString)aType delegate:(id)aDelegate didReadSelector:(SEL)aDidReadSelector contextInfo:(id)aContextInfo
424{
425    [_readConnection cancel];
426
427    // FIXME: Oh man is this every looking for trouble, we need to handle login at the Cappuccino level, with HTTP Errors.
428    _readConnection = [CPURLConnection connectionWithRequest:[CPURLRequest requestWithURL:anAbsoluteURL] delegate:self];
429
430    _readConnection.session = _CPReadSessionMake(aType, aDelegate, aDidReadSelector, aContextInfo);
431}
432
433/*!
434    Returns the path to the document's file.
435*/
436- (CPURL)fileURL
437{
438    return _fileURL;
439}
440
441/*!
442    Sets the path to the document's file.
443    @param aFileURL the path to the document's file
444*/
445- (void)setFileURL:(CPURL)aFileURL
446{
447    if (_fileURL === aFileURL)
448        return;
449
450    _fileURL = aFileURL;
451
452    [_windowControllers makeObjectsPerformSelector:@selector(synchronizeWindowTitleWithDocumentName)];
453}
454
455/*!
456    Saves the document to the specified URL. Notifies the provided delegate
457    with the provided selector and context info afterwards.
458    @param anAbsoluteURL the url to write the document data to
459    @param aTypeName the document type
460    @param aSaveOperation the type of save operation
461    @param aDelegate the delegate to notify after saving
462    @param aDidSaveSelector the selector to send the delegate
463    @param aContextInfo context info that gets passed to the delegate
464*/
465- (void)saveToURL:(CPURL)anAbsoluteURL ofType:(CPString)aTypeName forSaveOperation:(CPSaveOperationType)aSaveOperation delegate:(id)aDelegate didSaveSelector:(SEL)aDidSaveSelector contextInfo:(id)aContextInfo
466{
467    var data = [self dataOfType:[self fileType] error:nil],
468        oldChangeCount = _changeCount;
469
470    _writeRequest = [CPURLRequest requestWithURL:anAbsoluteURL];
471
472    // FIXME: THIS IS WRONG! We need a way to decide
473    if ([CPPlatform isBrowser])
474        [_writeRequest setHTTPMethod:@"POST"];
475    else
476        [_writeRequest setHTTPMethod:@"PUT"];
477
478    [_writeRequest setHTTPBody:[data rawString]];
479
480    [_writeRequest setValue:@"close" forHTTPHeaderField:@"Connection"];
481
482    if (aSaveOperation === CPSaveOperation)
483        [_writeRequest setValue:@"true" forHTTPHeaderField:@"x-cappuccino-overwrite"];
484
485    if (aSaveOperation !== CPSaveToOperation)
486        [self updateChangeCount:CPChangeCleared];
487
488    // FIXME: Oh man is this every looking for trouble, we need to handle login at the Cappuccino level, with HTTP Errors.
489    var connection = [CPURLConnection connectionWithRequest:_writeRequest delegate:self];
490
491    connection.session = _CPSaveSessionMake(anAbsoluteURL, aSaveOperation, oldChangeCount, aDelegate, aDidSaveSelector, aContextInfo, connection);
492}
493
494/*
495    Implemented as a delegate method for CPURLConnection
496    @ignore
497*/
498- (void)connection:(CPURLConnection)aConnection didReceiveResponse:(CPURLResponse)aResponse
499{
500    // If we got this far and it wasn't an HTTP request, then everything is fine.
501    if (![aResponse isKindOfClass:[CPHTTPURLResponse class]])
502        return;
503
504    var statusCode = [aResponse statusCode];
505
506    // Nothing to do if everything is hunky dory.
507    if (statusCode === 200)
508        return;
509
510    var session = aConnection.session;
511
512    if (aConnection == _readConnection)
513    {
514        [aConnection cancel];
515
516        alert("There was an error retrieving the document.");
517
518        objj_msgSend(session.delegate, session.didReadSelector, self, NO, session.contextInfo);
519    }
520    else
521    {
522        // 409: Conflict, in Cappuccino, overwrite protection for documents.
523        if (statusCode == 409)
524        {
525            [aConnection cancel];
526
527            if (confirm("There already exists a file with that name, would you like to overwrite it?"))
528            {
529                [_writeRequest setValue:@"true" forHTTPHeaderField:@"x-cappuccino-overwrite"];
530
531                [aConnection start];
532            }
533            else
534            {
535                if (session.saveOperation != CPSaveToOperation)
536                {
537                    _changeCount += session.changeCount;
538                    [_windowControllers makeObjectsPerformSelector:@selector(setDocumentEdited:) withObject:[self isDocumentEdited]];
539                }
540
541                _writeRequest = nil;
542
543                objj_msgSend(session.delegate, session.didSaveSelector, self, NO, session.contextInfo);
544                [self _sendDocumentSavedNotification:NO];
545            }
546        }
547    }
548}
549
550/*
551    Implemented as a delegate method for CPURLConnection
552    @ignore
553*/
554- (void)connection:(CPURLConnection)aConnection didReceiveData:(CPString)aData
555{
556    var session = aConnection.session;
557
558    // READ
559    if (aConnection == _readConnection)
560    {
561        [self readFromData:[CPData dataWithRawString:aData] ofType:session.fileType error:nil];
562
563        objj_msgSend(session.delegate, session.didReadSelector, self, YES, session.contextInfo);
564    }
565    else
566    {
567        if (session.saveOperation != CPSaveToOperation)
568            [self setFileURL:session.absoluteURL];
569
570        _writeRequest = nil;
571
572        objj_msgSend(session.delegate, session.didSaveSelector, self, YES, session.contextInfo);
573        [self _sendDocumentSavedNotification:YES];
574    }
575}
576
577/*
578    Implemented as a delegate method for CPURLConnection
579    @ignore
580*/
581- (void)connection:(CPURLConnection)aConnection didFailWithError:(CPError)anError
582{
583    var session = aConnection.session;
584
585    if (_readConnection == aConnection)
586        objj_msgSend(session.delegate, session.didReadSelector, self, NO, session.contextInfo);
587
588    else
589    {
590        if (session.saveOperation != CPSaveToOperation)
591        {
592            _changeCount += session.changeCount;
593            [_windowControllers makeObjectsPerformSelector:@selector(setDocumentEdited:) withObject:[self isDocumentEdited]];
594        }
595
596        _writeRequest = nil;
597
598        alert("There was an error saving the document.");
599
600        objj_msgSend(session.delegate, session.didSaveSelector, self, NO, session.contextInfo);
601        [self _sendDocumentSavedNotification:NO];
602    }
603}
604
605/*
606    Implemented as a delegate method for CPURLConnection
607    @ignore
608*/
609- (void)connectionDidFinishLoading:(CPURLConnection)aConnection
610{
611    if (_readConnection == aConnection)
612        _readConnection = nil;
613}
614
615// Managing Document Status
616/*!
617    Returns \c YES if there are any unsaved changes.
618*/
619- (BOOL)isDocumentEdited
620{
621    return _changeCount != 0;
622}
623
624/*!
625    Updates the number of unsaved changes to the document.
626    @param aChangeType a new document change to apply
627*/
628- (void)updateChangeCount:(CPDocumentChangeType)aChangeType
629{
630    if (aChangeType == CPChangeDone)
631        ++_changeCount;
632    else if (aChangeType == CPChangeUndone)
633        --_changeCount;
634    else if (aChangeType == CPChangeCleared)
635        _changeCount = 0;
636    /*else if (aChangeType == CPCHangeReadOtherContents)
637
638    else if (aChangeType == CPChangeAutosaved)*/
639
640    [_windowControllers makeObjectsPerformSelector:@selector(setDocumentEdited:) withObject:[self isDocumentEdited]];
641}
642
643// Managing File Types
644/*!
645    Sets the document's file type
646    @param aType the document's type
647*/
648- (void)setFileType:(CPString)aType
649{
650    _fileType = aType;
651}
652
653/*!
654    Returns the document's file type
655*/
656- (CPString)fileType
657{
658    return _fileType;
659}
660
661// Working with Undo Manager
662/*!
663    Returns \c YES if the document has a
664    CPUndoManager.
665*/
666- (BOOL)hasUndoManager
667{
668    return _hasUndoManager;
669}
670
671/*!
672    Sets whether the document should have a CPUndoManager.
673    @param aFlag \c YES makes the document have an undo manager
674*/
675- (void)setHasUndoManager:(BOOL)aFlag
676{
677    if (_hasUndoManager == aFlag)
678        return;
679
680    _hasUndoManager = aFlag;
681
682    if (!_hasUndoManager)
683        [self setUndoManager:nil];
684}
685
686/* @ignore */
687- (void)_undoManagerWillCloseGroup:(CPNotification)aNotification
688{
689    var undoManager = [aNotification object];
690
691    if ([undoManager isUndoing] || [undoManager isRedoing])
692        return;
693
694    [self updateChangeCount:CPChangeDone];
695}
696
697/* @ignore */
698- (void)_undoManagerDidUndoChange:(CPNotification)aNotification
699{
700    [self updateChangeCount:CPChangeUndone];
701}
702
703/* @ignore */
704- (void)_undoManagerDidRedoChange:(CPNotification)aNotification
705{
706    [self updateChangeCount:CPChangeDone];
707}
708
709/*
710    Sets the document's undo manager. This method will add the
711    undo manager as an observer to the notification center.
712    @param anUndoManager the new undo manager for the document
713*/
714- (void)setUndoManager:(CPUndoManager)anUndoManager
715{
716    var defaultCenter = [CPNotificationCenter defaultCenter];
717
718    if (_undoManager)
719    {
720        [defaultCenter removeObserver:self
721                                 name:CPUndoManagerDidUndoChangeNotification
722                               object:_undoManager];
723
724        [defaultCenter removeObserver:self
725                                 name:CPUndoManagerDidRedoChangeNotification
726                               object:_undoManager];
727
728        [defaultCenter removeObserver:self
729                                 name:CPUndoManagerWillCloseUndoGroupNotification
730                               object:_undoManager];
731    }
732
733    _undoManager = anUndoManager;
734
735    if (_undoManager)
736    {
737
738        [defaultCenter addObserver:self
739                          selector:@selector(_undoManagerDidUndoChange:)
740                              name:CPUndoManagerDidUndoChangeNotification
741                            object:_undoManager];
742
743        [defaultCenter addObserver:self
744                          selector:@selector(_undoManagerDidRedoChange:)
745                              name:CPUndoManagerDidRedoChangeNotification
746                            object:_undoManager];
747
748        [defaultCenter addObserver:self
749                          selector:@selector(_undoManagerWillCloseGroup:)
750                              name:CPUndoManagerWillCloseUndoGroupNotification
751                            object:_undoManager];
752    }
753}
754
755/*!
756    Returns the document's undo manager. If the document
757    should have one, but the manager is \c nil, it
758    will be created and then returned.
759    @return the document's undo manager
760*/
761- (CPUndoManager)undoManager
762{
763    if (_hasUndoManager && !_undoManager)
764        [self setUndoManager:[[CPUndoManager alloc] init]];
765
766    return _undoManager;
767}
768
769/*
770    Implemented as a delegate of a CPWindow
771    @ignore
772*/
773- (CPUndoManager)windowWillReturnUndoManager:(CPWindow)aWindow
774{
775    return [self undoManager];
776}
777
778// Handling User Actions
779/*!
780    Saves the document. If the document does not
781    have a file path to save to (\c fileURL)
782    then \c -saveDocumentAs: will be called.
783    @param aSender the object requesting the save
784*/
785- (void)saveDocument:(id)aSender
786{
787    [self saveDocumentWithDelegate:nil didSaveSelector:nil contextInfo:nil];
788}
789
790- (void)saveDocumentWithDelegate:(id)delegate didSaveSelector:(SEL)didSaveSelector contextInfo:(Object)contextInfo
791{
792    if (_fileURL)
793    {
794        [[CPNotificationCenter defaultCenter]
795            postNotificationName:CPDocumentWillSaveNotification
796                          object:self];
797
798        [self saveToURL:_fileURL ofType:[self fileType] forSaveOperation:CPSaveOperation delegate:delegate didSaveSelector:didSaveSelector contextInfo:contextInfo];
799    }
800    else
801        [self _saveDocumentAsWithDelegate:delegate didSaveSelector:didSaveSelector contextInfo:contextInfo];
802}
803
804/*!
805    Saves the document to a user specified path.
806    @param aSender the object requesting the operation
807*/
808- (void)saveDocumentAs:(id)aSender
809{
810    [self _saveDocumentAsWithDelegate:nil didSaveSelector:nil contextInfo:nil];
811}
812
813- (void)_saveDocumentAsWithDelegate:(id)delegate didSaveSelector:(SEL)didSaveSelector contextInfo:(Object)contextInfo
814{
815    var savePanel = [CPSavePanel savePanel],
816        response = [savePanel runModal];
817
818    if (!response)
819        return;
820
821    var saveURL = [savePanel URL];
822
823    [[CPNotificationCenter defaultCenter]
824        postNotificationName:CPDocumentWillSaveNotification
825                      object:self];
826
827    [self saveToURL:saveURL ofType:[self fileType] forSaveOperation:CPSaveAsOperation delegate:delegate didSaveSelector:didSaveSelector contextInfo:contextInfo];
828}
829
830/*
831    @ignore
832*/
833- (void)_sendDocumentSavedNotification:(BOOL)didSave
834{
835    if (didSave)
836        [[CPNotificationCenter defaultCenter]
837            postNotificationName:CPDocumentDidSaveNotification
838                          object:self];
839    else
840        [[CPNotificationCenter defaultCenter]
841            postNotificationName:CPDocumentDidFailToSaveNotification
842                          object:self];
843}
844
845@end
846
847@implementation CPDocument (ClosingDocuments)
848
849- (void)close
850{
851    [_windowControllers makeObjectsPerformSelector:@selector(removeDocumentAndCloseIfNecessary:) withObject:self];
852    [[CPDocumentController sharedDocumentController] removeDocument:self];
853}
854
855- (void)shouldCloseWindowController:(CPWindowController)controller delegate:(id)delegate shouldCloseSelector:(SEL)selector contextInfo:(Object)info
856{
857    if ([controller shouldCloseDocument] || ([_windowControllers count] < 2 && [_windowControllers indexOfObject:controller] !== CPNotFound))
858        [self canCloseDocumentWithDelegate:self shouldCloseSelector:@selector(_document:shouldClose:context:) contextInfo:{delegate:delegate, selector:selector, context:info}];
859
860    else if ([delegate respondsToSelector:selector])
861        objj_msgSend(delegate, selector, self, YES, info);
862}
863
864- (void)_document:(CPDocument)aDocument shouldClose:(BOOL)shouldClose context:(Object)context
865{
866    if (aDocument === self && shouldClose)
867        [self close];
868
869    objj_msgSend(context.delegate, context.selector, aDocument, shouldClose, context.context);
870}
871
872- (void)canCloseDocumentWithDelegate:(id)aDelegate shouldCloseSelector:(SEL)aSelector contextInfo:(Object)context
873{
874    if (![self isDocumentEdited])
875        return [aDelegate respondsToSelector:aSelector] && objj_msgSend(aDelegate, aSelector, self, YES, context);
876
877    _canCloseAlert = [[CPAlert alloc] init];
878
879    [_canCloseAlert setDelegate:self];
880    [_canCloseAlert setAlertStyle:CPWarningAlertStyle];
881    [_canCloseAlert setTitle:@"Unsaved Document"];
882    [_canCloseAlert setMessageText:@"Do you want to save the changes you've made to the document \"" + ([self displayName] || [self fileName]) + "\"?"];
883
884    [_canCloseAlert addButtonWithTitle:@"Save"];
885    [_canCloseAlert addButtonWithTitle:@"Cancel"];
886    [_canCloseAlert addButtonWithTitle:@"Don't Save"];
887
888    _canCloseAlert._context = {delegate:aDelegate, selector:aSelector, context:context};
889
890    [_canCloseAlert runModal];
891}
892
893- (void)alertDidEnd:(CPAlert)alert returnCode:(int)returnCode
894{
895    if (alert !== _canCloseAlert)
896        return;
897
898    var delegate = alert._context.delegate,
899        selector = alert._context.selector,
900        context = alert._context.context;
901
902    if (returnCode === 0)
903        [self saveDocumentWithDelegate:delegate didSaveSelector:selector contextInfo:context];
904    else
905        objj_msgSend(delegate, selector, self, returnCode === 2, context);
906
907    _canCloseAlert = nil;
908}
909
910@end
911
912var _CPReadSessionMake = function(aType, aDelegate, aDidReadSelector, aContextInfo)
913{
914    return { fileType:aType, delegate:aDelegate, didReadSelector:aDidReadSelector, contextInfo:aContextInfo };
915};
916
917var _CPSaveSessionMake = function(anAbsoluteURL, aSaveOperation, aChangeCount, aDelegate, aDidSaveSelector, aContextInfo, aConnection)
918{
919    return { absoluteURL:anAbsoluteURL, saveOperation:aSaveOperation, changeCount:aChangeCount, delegate:aDelegate, didSaveSelector:aDidSaveSelector, contextInfo:aContextInfo, connection:aConnection };
920};