API  0.9.8
 All Classes Files Functions Variables Typedefs Macros Groups Pages
CPDocument.j
Go to the documentation of this 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 
24 
25 
26 @global CPApp
27 
28 
29 /*
30  @global
31  @group CPSaveOperationType
32 */
34 /*
35  @global
36  @group CPSaveOperationType
37 */
39 /*
40  @global
41  @group CPSaveOperationType
42 */
44 /*
45  @global
46  @group CPSaveOperationType
47 */
49 
50 /*
51  @global
52  @group CPDocumentChangeType
53 */
55 /*
56  @global
57  @group CPDocumentChangeType
58 */
60 /*
61  @global
62  @group CPDocumentChangeType
63 */
65 /*
66  @global
67  @group CPDocumentChangeType
68 */
70 /*
71  @global
72  @group CPDocumentChangeType
73 */
75 
76 CPDocumentWillSaveNotification = @"CPDocumentWillSaveNotification";
77 CPDocumentDidSaveNotification = @"CPDocumentDidSaveNotification";
78 CPDocumentDidFailToSaveNotification = @"CPDocumentDidFailToSaveNotification";
79 
81 
90 @implementation CPDocument : CPResponder
91 {
92  CPWindow _window; // For outlet purposes.
93  CPView _view; // For outlet purposes
94  CPDictionary _viewControllersForWindowControllers;
95 
96  CPURL _fileURL;
97  CPString _fileType;
98  CPArray _windowControllers;
99  unsigned _untitledDocumentIndex;
100 
101  BOOL _hasUndoManager;
102  CPUndoManager _undoManager;
103 
104  int _changeCount;
105 
106  CPURLConnection _readConnection;
107  CPURLRequest _writeRequest;
108 
109  CPAlert _canCloseAlert;
110 }
111 
116 - (id)init
117 {
118  self = [super init];
119 
120  if (self)
121  {
122  _windowControllers = [];
123  _viewControllersForWindowControllers = @{};
124 
125  _hasUndoManager = YES;
126  _changeCount = 0;
127 
128  [self setNextResponder:CPApp];
129  }
130 
131  return self;
132 }
133 
140 - (id)initWithType:(CPString)aType error:(/*{*/CPError/*}*/)anError
141 {
142  self = [self init];
143 
144  if (self)
145  [self setFileType:aType];
146 
147  return self;
148 }
149 
161 - (id)initWithContentsOfURL:(CPURL)anAbsoluteURL ofType:(CPString)aType delegate:(id)aDelegate didReadSelector:(SEL)aDidReadSelector contextInfo:(id)aContextInfo
162 {
163  self = [self init];
164 
165  if (self)
166  {
167  [self setFileURL:anAbsoluteURL];
168  [self setFileType:aType];
169 
170  [self readFromURL:anAbsoluteURL ofType:aType delegate:aDelegate didReadSelector:aDidReadSelector contextInfo:aContextInfo];
171  }
172 
173  return self;
174 }
175 
186 - (id)initForURL:(CPURL)anAbsoluteURL withContentsOfURL:(CPURL)absoluteContentsURL ofType:(CPString)aType delegate:(id)aDelegate didReadSelector:(SEL)aDidReadSelector contextInfo:(id)aContextInfo
187 {
188  self = [self init];
189 
190  if (self)
191  {
192  [self setFileURL:anAbsoluteURL];
193  [self setFileType:aType];
194 
195  [self readFromURL:absoluteContentsURL ofType:aType delegate:aDelegate didReadSelector:aDidReadSelector contextInfo:aContextInfo];
196  }
197 
198  return self;
199 }
200 
209 - (CPData)dataOfType:(CPString)aType error:(/*{*/CPError/*}*/)anError
210 {
211  [CPException raise:CPUnsupportedMethodException
212  reason:"dataOfType:error: must be overridden by the document subclass."];
213 }
214 
224 - (void)readFromData:(CPData)aData ofType:(CPString)aType error:(CPError)anError
225 {
226  [CPException raise:CPUnsupportedMethodException
227  reason:"readFromData:ofType: must be overridden by the document subclass."];
228 }
229 
230 - (void)viewControllerWillLoadCib:(CPViewController)aViewController
231 {
232 }
233 
234 - (void)viewControllerDidLoadCib:(CPViewController)aViewController
235 {
236 }
237 
238 // Creating and managing window controllers
242 - (void)makeWindowControllers
243 {
245 }
246 
247 - (void)makeViewAndWindowControllers
248 {
249  var viewCibName = [self viewCibName],
250  windowCibName = [self windowCibName],
251  viewController = nil,
252  windowController = nil;
253 
254  // Create our view controller if we have a cib for it.
255  if ([viewCibName length])
256  viewController = [[CPViewController alloc] initWithCibName:viewCibName bundle:nil owner:self];
257 
258  // From a cib if we have one.
259  if ([windowCibName length])
260  windowController = [[CPWindowController alloc] initWithWindowCibName:windowCibName owner:self];
261 
262  // If not you get a standard window capable of displaying multiple documents and view
263  else if (viewController)
264  {
265  var view = [viewController view],
266  viewFrame = [view frame];
267 
268  viewFrame.origin = CGPointMake(50, 50);
269 
270  var theWindow = [[CPWindow alloc] initWithContentRect:viewFrame styleMask:CPTitledWindowMask | CPClosableWindowMask | CPMiniaturizableWindowMask | CPResizableWindowMask];
271 
272  windowController = [[CPWindowController alloc] initWithWindow:theWindow];
273  }
274 
275  if (windowController && viewController)
276  [windowController setSupportsMultipleDocuments:YES];
277 
278  if (windowController)
279  [self addWindowController:windowController];
280 
281  if (viewController)
282  [self addViewController:viewController forWindowController:windowController];
283 }
284 
288 - (CPArray)windowControllers
289 {
290  return _windowControllers;
291 }
292 
298 - (void)addWindowController:(CPWindowController)aWindowController
299 {
300  [_windowControllers addObject:aWindowController];
301 
302  if ([aWindowController document] !== self)
303  [aWindowController setDocument:self];
304 }
305 
311 - (void)removeWindowController:(CPWindowController)aWindowController
312 {
313  if (aWindowController)
314  [_windowControllers removeObject:aWindowController];
315 
316  if ([aWindowController document] === self)
317  [aWindowController setDocument:nil];
318 }
319 
320 - (CPView)view
321 {
322  return _view;
323 }
324 
325 - (CPArray)viewControllers
326 {
327  return [_viewControllersForWindowControllers allValues];
328 }
329 
330 - (void)addViewController:(CPViewController)aViewController forWindowController:(CPWindowController)aWindowController
331 {
332  // FIXME: exception if we don't own the window controller?
333  [_viewControllersForWindowControllers setObject:aViewController forKey:[aWindowController UID]];
334 
335  if ([aWindowController document] === self)
336  [aWindowController setViewController:aViewController];
337 }
338 
339 - (void)removeViewController:(CPViewController)aViewController
340 {
341  [_viewControllersForWindowControllers removeObject:aViewController];
342 }
343 
344 - (CPViewController)viewControllerForWindowController:(CPWindowController)aWindowController
345 {
346  return [_viewControllersForWindowControllers objectForKey:[aWindowController UID]];
347 }
348 
349 // Managing Document Windows
353 - (void)showWindows
354 {
355  [_windowControllers makeObjectsPerformSelector:@selector(setDocument:) withObject:self];
356  [_windowControllers makeObjectsPerformSelector:@selector(showWindow:) withObject:self];
357 }
358 
362 - (CPString)displayName
363 {
364  if (_fileURL)
365  return [_fileURL lastPathComponent];
366 
367  if (!_untitledDocumentIndex)
368  _untitledDocumentIndex = ++CPDocumentUntitledCount;
369 
370  if (_untitledDocumentIndex == 1)
371  return @"Untitled";
372 
373  return @"Untitled " + _untitledDocumentIndex;
374 }
375 
376 - (CPString)viewCibName
377 {
378  return nil;
379 }
380 
384 - (CPString)windowCibName
385 {
386  return nil;
387 }
388 
393 - (void)windowControllerDidLoadCib:(CPWindowController)aWindowController
394 {
395 }
396 
401 - (void)windowControllerWillLoadCib:(CPWindowController)aWindowController
402 {
403 }
404 
405 // Reading from and Writing to URLs
414 - (void)readFromURL:(CPURL)anAbsoluteURL ofType:(CPString)aType delegate:(id)aDelegate didReadSelector:(SEL)aDidReadSelector contextInfo:(id)aContextInfo
415 {
416  [_readConnection cancel];
417 
418  // FIXME: Oh man is this every looking for trouble, we need to handle login at the Cappuccino level, with HTTP Errors.
419  _readConnection = [CPURLConnection connectionWithRequest:[CPURLRequest requestWithURL:anAbsoluteURL] delegate:self];
420 
421  _readConnection.session = _CPReadSessionMake(aType, aDelegate, aDidReadSelector, aContextInfo);
422 }
423 
427 - (CPURL)fileURL
428 {
429  return _fileURL;
430 }
431 
436 - (void)setFileURL:(CPURL)aFileURL
437 {
438  if (_fileURL === aFileURL)
439  return;
440 
441  _fileURL = aFileURL;
442 
443  [_windowControllers makeObjectsPerformSelector:@selector(synchronizeWindowTitleWithDocumentName)];
444 }
445 
456 - (void)saveToURL:(CPURL)anAbsoluteURL ofType:(CPString)aTypeName forSaveOperation:(CPSaveOperationType)aSaveOperation delegate:(id)aDelegate didSaveSelector:(SEL)aDidSaveSelector contextInfo:(id)aContextInfo
457 {
458  var data = [self dataOfType:[self fileType] error:nil],
459  oldChangeCount = _changeCount;
460 
461  _writeRequest = [CPURLRequest requestWithURL:anAbsoluteURL];
462 
463  // FIXME: THIS IS WRONG! We need a way to decide
464  if ([CPPlatform isBrowser])
465  [_writeRequest setHTTPMethod:@"POST"];
466  else
467  [_writeRequest setHTTPMethod:@"PUT"];
468 
469  [_writeRequest setHTTPBody:[data rawString]];
470 
471  [_writeRequest setValue:@"close" forHTTPHeaderField:@"Connection"];
472 
473  if (aSaveOperation === CPSaveOperation)
474  [_writeRequest setValue:@"true" forHTTPHeaderField:@"x-cappuccino-overwrite"];
475 
476  if (aSaveOperation !== CPSaveToOperation)
477  [self updateChangeCount:CPChangeCleared];
478 
479  // FIXME: Oh man is this every looking for trouble, we need to handle login at the Cappuccino level, with HTTP Errors.
480  var connection = [CPURLConnection connectionWithRequest:_writeRequest delegate:self];
481 
482  connection.session = _CPSaveSessionMake(anAbsoluteURL, aSaveOperation, oldChangeCount, aDelegate, aDidSaveSelector, aContextInfo, connection);
483 }
484 
485 /*
486  Implemented as a delegate method for CPURLConnection
487  @ignore
488 */
489 - (void)connection:(CPURLConnection)aConnection didReceiveResponse:(CPURLResponse)aResponse
490 {
491  // If we got this far and it wasn't an HTTP request, then everything is fine.
492  if (![aResponse isKindOfClass:[CPHTTPURLResponse class]])
493  return;
494 
495  var statusCode = [aResponse statusCode];
496 
497  // Nothing to do if everything is hunky dory.
498  if (statusCode === 200)
499  return;
500 
501  var session = aConnection.session;
502 
503  if (aConnection == _readConnection)
504  {
505  [aConnection cancel];
506 
507  alert("There was an error retrieving the document.");
508 
509  objj_msgSend(session.delegate, session.didReadSelector, self, NO, session.contextInfo);
510  }
511  else
512  {
513  // 409: Conflict, in Cappuccino, overwrite protection for documents.
514  if (statusCode == 409)
515  {
516  [aConnection cancel];
517 
518  if (confirm("There already exists a file with that name, would you like to overwrite it?"))
519  {
520  [_writeRequest setValue:@"true" forHTTPHeaderField:@"x-cappuccino-overwrite"];
521 
522  [aConnection start];
523  }
524  else
525  {
526  if (session.saveOperation != CPSaveToOperation)
527  {
528  _changeCount += session.changeCount;
529  [_windowControllers makeObjectsPerformSelector:@selector(setDocumentEdited:) withObject:[self isDocumentEdited]];
530  }
531 
532  _writeRequest = nil;
533 
534  objj_msgSend(session.delegate, session.didSaveSelector, self, NO, session.contextInfo);
535  [self _sendDocumentSavedNotification:NO];
536  }
537  }
538  }
539 }
540 
541 /*
542  Implemented as a delegate method for CPURLConnection
543  @ignore
544 */
545 - (void)connection:(CPURLConnection)aConnection didReceiveData:(CPString)aData
546 {
547  var session = aConnection.session;
548 
549  // READ
550  if (aConnection == _readConnection)
551  {
552  [self readFromData:[CPData dataWithRawString:aData] ofType:session.fileType error:nil];
553 
554  objj_msgSend(session.delegate, session.didReadSelector, self, YES, session.contextInfo);
555  }
556  else
557  {
558  if (session.saveOperation != CPSaveToOperation)
559  [self setFileURL:session.absoluteURL];
560 
561  _writeRequest = nil;
562 
563  objj_msgSend(session.delegate, session.didSaveSelector, self, YES, session.contextInfo);
564  [self _sendDocumentSavedNotification:YES];
565  }
566 }
567 
568 /*
569  Implemented as a delegate method for CPURLConnection
570  @ignore
571 */
572 - (void)connection:(CPURLConnection)aConnection didFailWithError:(CPError)anError
573 {
574  var session = aConnection.session;
575 
576  if (_readConnection == aConnection)
577  objj_msgSend(session.delegate, session.didReadSelector, self, NO, session.contextInfo);
578 
579  else
580  {
581  if (session.saveOperation != CPSaveToOperation)
582  {
583  _changeCount += session.changeCount;
584  [_windowControllers makeObjectsPerformSelector:@selector(setDocumentEdited:) withObject:[self isDocumentEdited]];
585  }
586 
587  _writeRequest = nil;
588 
589  alert("There was an error saving the document.");
590 
591  objj_msgSend(session.delegate, session.didSaveSelector, self, NO, session.contextInfo);
592  [self _sendDocumentSavedNotification:NO];
593  }
594 }
595 
596 /*
597  Implemented as a delegate method for CPURLConnection
598  @ignore
599 */
600 - (void)connectionDidFinishLoading:(CPURLConnection)aConnection
601 {
602  if (_readConnection == aConnection)
603  _readConnection = nil;
604 }
605 
606 // Managing Document Status
610 - (BOOL)isDocumentEdited
611 {
612  return _changeCount != 0;
613 }
614 
619 - (void)updateChangeCount:(CPDocumentChangeType)aChangeType
620 {
621  if (aChangeType == CPChangeDone)
622  ++_changeCount;
623  else if (aChangeType == CPChangeUndone)
624  --_changeCount;
625  else if (aChangeType == CPChangeCleared)
626  _changeCount = 0;
627  /*else if (aChangeType == CPCHangeReadOtherContents)
628 
629  else if (aChangeType == CPChangeAutosaved)*/
630 
631  [_windowControllers makeObjectsPerformSelector:@selector(setDocumentEdited:) withObject:[self isDocumentEdited]];
632 }
633 
634 // Managing File Types
639 - (void)setFileType:(CPString)aType
640 {
641  _fileType = aType;
642 }
643 
647 - (CPString)fileType
648 {
649  return _fileType;
650 }
651 
652 // Working with Undo Manager
657 - (BOOL)hasUndoManager
658 {
659  return _hasUndoManager;
660 }
661 
666 - (void)setHasUndoManager:(BOOL)aFlag
667 {
668  if (_hasUndoManager == aFlag)
669  return;
670 
671  _hasUndoManager = aFlag;
672 
673  if (!_hasUndoManager)
674  [self setUndoManager:nil];
675 }
676 
677 /* @ignore */
678 - (void)_undoManagerWillCloseGroup:(CPNotification)aNotification
679 {
680  var undoManager = [aNotification object];
681 
682  if ([undoManager isUndoing] || [undoManager isRedoing])
683  return;
684 
685  [self updateChangeCount:CPChangeDone];
686 }
687 
688 /* @ignore */
689 - (void)_undoManagerDidUndoChange:(CPNotification)aNotification
690 {
691  [self updateChangeCount:CPChangeUndone];
692 }
693 
694 /* @ignore */
695 - (void)_undoManagerDidRedoChange:(CPNotification)aNotification
696 {
697  [self updateChangeCount:CPChangeDone];
698 }
699 
700 /*
701  Sets the document's undo manager. This method will add the
702  undo manager as an observer to the notification center.
703  @param anUndoManager the new undo manager for the document
704 */
705 - (void)setUndoManager:(CPUndoManager)anUndoManager
706 {
707  var defaultCenter = [CPNotificationCenter defaultCenter];
708 
709  if (_undoManager)
710  {
711  [defaultCenter removeObserver:self
712  name:CPUndoManagerDidUndoChangeNotification
713  object:_undoManager];
714 
715  [defaultCenter removeObserver:self
716  name:CPUndoManagerDidRedoChangeNotification
717  object:_undoManager];
718 
719  [defaultCenter removeObserver:self
720  name:CPUndoManagerWillCloseUndoGroupNotification
721  object:_undoManager];
722  }
723 
724  _undoManager = anUndoManager;
725 
726  if (_undoManager)
727  {
728 
729  [defaultCenter addObserver:self
730  selector:@selector(_undoManagerDidUndoChange:)
731  name:CPUndoManagerDidUndoChangeNotification
732  object:_undoManager];
733 
734  [defaultCenter addObserver:self
735  selector:@selector(_undoManagerDidRedoChange:)
736  name:CPUndoManagerDidRedoChangeNotification
737  object:_undoManager];
738 
739  [defaultCenter addObserver:self
740  selector:@selector(_undoManagerWillCloseGroup:)
741  name:CPUndoManagerWillCloseUndoGroupNotification
742  object:_undoManager];
743  }
744 }
745 
752 - (CPUndoManager)undoManager
753 {
754  if (_hasUndoManager && !_undoManager)
755  [self setUndoManager:[[CPUndoManager alloc] init]];
756 
757  return _undoManager;
758 }
759 
760 /*
761  Implemented as a delegate of a CPWindow
762  @ignore
763 */
764 - (CPUndoManager)windowWillReturnUndoManager:(CPWindow)aWindow
765 {
766  return [self undoManager];
767 }
768 
769 // Handling User Actions
776 - (void)saveDocument:(id)aSender
777 {
779 }
780 
781 - (void)saveDocumentWithDelegate:(id)delegate didSaveSelector:(SEL)didSaveSelector contextInfo:(Object)contextInfo
782 {
783  if (_fileURL)
784  {
786  postNotificationName:CPDocumentWillSaveNotification
787  object:self];
788 
789  [self saveToURL:_fileURL ofType:[self fileType] forSaveOperation:CPSaveOperation delegate:delegate didSaveSelector:didSaveSelector contextInfo:contextInfo];
790  }
791  else
792  [self _saveDocumentAsWithDelegate:delegate didSaveSelector:didSaveSelector contextInfo:contextInfo];
793 }
794 
799 - (void)saveDocumentAs:(id)aSender
800 {
801  [self _saveDocumentAsWithDelegate:nil didSaveSelector:nil contextInfo:nil];
802 }
803 
804 - (void)_saveDocumentAsWithDelegate:(id)delegate didSaveSelector:(SEL)didSaveSelector contextInfo:(Object)contextInfo
805 {
806  var savePanel = [CPSavePanel savePanel],
807  response = [savePanel runModal];
808 
809  if (!response)
810  return;
811 
812  var saveURL = [savePanel URL];
813 
815  postNotificationName:CPDocumentWillSaveNotification
816  object:self];
817 
818  [self saveToURL:saveURL ofType:[self fileType] forSaveOperation:CPSaveAsOperation delegate:delegate didSaveSelector:didSaveSelector contextInfo:contextInfo];
819 }
820 
821 /*
822  @ignore
823 */
824 - (void)_sendDocumentSavedNotification:(BOOL)didSave
825 {
826  if (didSave)
828  postNotificationName:CPDocumentDidSaveNotification
829  object:self];
830  else
832  postNotificationName:CPDocumentDidFailToSaveNotification
833  object:self];
834 }
835 
836 @end
837 
839 
840 - (void)close
841 {
842  [_windowControllers makeObjectsPerformSelector:@selector(removeDocumentAndCloseIfNecessary:) withObject:self];
843  [[CPDocumentController sharedDocumentController] removeDocument:self];
844 }
845 
846 - (void)shouldCloseWindowController:(CPWindowController)controller delegate:(id)delegate shouldCloseSelector:(SEL)selector contextInfo:(Object)info
847 {
848  if ([controller shouldCloseDocument] || ([_windowControllers count] < 2 && [_windowControllers indexOfObject:controller] !== CPNotFound))
849  [self canCloseDocumentWithDelegate:self shouldCloseSelector:@selector(_document:shouldClose:context:) contextInfo:{delegate:delegate, selector:selector, context:info}];
850 
851  else if ([delegate respondsToSelector:selector])
852  objj_msgSend(delegate, selector, self, YES, info);
853 }
854 
855 - (void)_document:(CPDocument)aDocument shouldClose:(BOOL)shouldClose context:(Object)context
856 {
857  if (aDocument === self && shouldClose)
858  [self close];
859 
860  objj_msgSend(context.delegate, context.selector, aDocument, shouldClose, context.context);
861 }
862 
863 - (void)canCloseDocumentWithDelegate:(id)aDelegate shouldCloseSelector:(SEL)aSelector contextInfo:(Object)context
864 {
865  if (![self isDocumentEdited])
866  return [aDelegate respondsToSelector:aSelector] && objj_msgSend(aDelegate, aSelector, self, YES, context);
867 
868  _canCloseAlert = [[CPAlert alloc] init];
869 
870  [_canCloseAlert setDelegate:self];
871  [_canCloseAlert setAlertStyle:CPWarningAlertStyle];
872  [_canCloseAlert setTitle:@"Unsaved Document"];
873  [_canCloseAlert setMessageText:@"Do you want to save the changes you've made to the document \"" + ([self displayName] || [self fileName]) + "\"?"];
874 
875  [_canCloseAlert addButtonWithTitle:@"Save"];
876  [_canCloseAlert addButtonWithTitle:@"Cancel"];
877  [_canCloseAlert addButtonWithTitle:@"Don't Save"];
878 
879  _canCloseAlert._context = {delegate:aDelegate, selector:aSelector, context:context};
880 
881  [_canCloseAlert runModal];
882 }
883 
884 - (void)alertDidEnd:(CPAlert)alert returnCode:(int)returnCode
885 {
886  if (alert !== _canCloseAlert)
887  return;
888 
889  var delegate = alert._context.delegate,
890  selector = alert._context.selector,
891  context = alert._context.context;
892 
893  if (returnCode === 0)
894  [self saveDocumentWithDelegate:delegate didSaveSelector:selector contextInfo:context];
895  else
896  objj_msgSend(delegate, selector, self, returnCode === 2, context);
897 
898  _canCloseAlert = nil;
899 }
900 
901 @end
902 
903 var _CPReadSessionMake = function(aType, aDelegate, aDidReadSelector, aContextInfo)
904 {
905  return { fileType:aType, delegate:aDelegate, didReadSelector:aDidReadSelector, contextInfo:aContextInfo };
906 };
907 
908 var _CPSaveSessionMake = function(anAbsoluteURL, aSaveOperation, aChangeCount, aDelegate, aDidSaveSelector, aContextInfo, aConnection)
909 {
910  return { absoluteURL:anAbsoluteURL, saveOperation:aSaveOperation, changeCount:aChangeCount, delegate:aDelegate, didSaveSelector:aDidSaveSelector, contextInfo:aContextInfo, connection:aConnection };
911 };