1//
2//  PXDocument.m
3//  Pixen
4//
5//  Created by Joe Osborn on Thu Sep 11 2003.
6//  Copyright (c) 2003 Open Sword Group. All rights reserved.
7//
8
9#import "PXDocument.h"
10#import "PXCanvasController.h"
11#import "PXCanvas.h"
12#import "PXPSDHandler.h"
13#import "PXPalette.h"
14#import "PXCanvasView.h"
15#import "PXCanvasPrintView.h"
16#ifdef __COCOA__
17#import "gif_lib.h"
18#endif
19#import "PXGifExporter.h"
20#import "PXLayer.h"
21#import "PXImage.h"
22#import "PXPixel.h"
23#ifdef __COCOA__
24#import <AppKit/NSAlert.h>
25#endif
26
27#ifndef __COCOA__
28#include "math.h"
29#endif
30
31
32NSString * PXDocumentOpened = @"PXDocumentOpenedNotificationName";
33NSString * PXDocumentClosed = @"PXDocumentClosedNotificationName";
34
35
36@interface NSData(GOLAdditions)
37
38- (NSArray *)getLines;
39
40@end
41
42@implementation NSData(GOLAdditions)
43
44- (NSArray *)getLines
45{
46	NSRange charRange, lineRange = NSMakeRange(0, 0);
47	char character;
48	char line[4096];
49	NSMutableArray *lines = [NSMutableArray array];
50	for (charRange = NSMakeRange(0, 1); charRange.location<[self length]; charRange.location++) {
51		[self getBytes:&character range:charRange];
52		if (character == '\n') {
53			lineRange.length = charRange.location - lineRange.location;
54			if (lineRange.length >= 4096) {
55				NSLog(@"-[NSData getLines]: Line longer than 4K, truncating...");
56				lineRange.length = 4095;
57			}
58			[self getBytes:&line range:lineRange];
59			line[lineRange.length] = '\0';
60			lineRange.location += lineRange.length + 1;
61			[lines addObject:[NSString stringWithUTF8String:line]];
62		}
63	}
64	return [[lines copy] autorelease];
65}
66
67@end
68
69@implementation PXDocument
70
71- (BOOL)rescheduleAutosave
72{
73	NSTimeInterval repeatTime = [[NSUserDefaults standardUserDefaults] floatForKey:@"PXAutosaveInterval"];
74	if (repeatTime == 0.0f) {
75		[[NSUserDefaults standardUserDefaults] setFloat:180.0 forKey:@"PXAutosaveInterval"];
76		repeatTime = 180.0f;
77	}
78	if (repeatTime <= 0 || ![[NSUserDefaults standardUserDefaults] boolForKey:@"PXAutosaveEnabled"]) {
79		return NO;
80	}
81
82	[[self retain] autorelease];
83	[autosaveTimer invalidate];
84	[autosaveTimer release];
85	autosaveTimer = [[NSTimer scheduledTimerWithTimeInterval:repeatTime target:self selector:@selector(autosave:) userInfo:nil repeats:NO] retain];
86	return YES;
87}
88
89- (id)init
90{
91    [super init];
92	canSave = YES;
93	canvas = [[PXCanvas alloc] init];
94	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cleanUpAutosaveFile:) name:NSApplicationWillTerminateNotification object:[NSApplication sharedApplication]];
95 	[self autosave:nil];
96    return self;
97}
98
99- (void)updateChangeCount:(NSDocumentChangeType)changeType
100{
101	if (!canSave) {
102		[super updateChangeCount:NSChangeCleared];
103	} else {
104		[super updateChangeCount:changeType];
105	}
106}
107
108- (void)setCanSave:(BOOL)saveable
109{
110	canSave = saveable;
111	if (!canSave) {
112		[self updateChangeCount:NSChangeCleared];
113	}
114}
115
116- (void)canCloseDocumentWithDelegate:(id)delegate shouldCloseSelector:(SEL)shouldCloseSelector contextInfo:(void *)contextInfo
117{
118	if (!canSave) {
119		NSInvocation *closedInvocation;
120		BOOL shouldClose = YES;
121		closedInvocation = [NSInvocation invocationWithMethodSignature:[delegate methodSignatureForSelector:shouldCloseSelector]];
122		[closedInvocation setSelector:shouldCloseSelector];
123		[closedInvocation setArgument:&self atIndex:2];
124		[closedInvocation setArgument:&shouldClose atIndex:3];
125		[closedInvocation setArgument:&contextInfo atIndex:4];
126		[closedInvocation invokeWithTarget:delegate];
127	} else {
128		[super canCloseDocumentWithDelegate:delegate shouldCloseSelector:shouldCloseSelector contextInfo:contextInfo];
129	}
130}
131
132- (IBAction)saveDocument:(id)sender
133{
134	if (!canSave) {
135		[self close];
136	} else {
137		[super saveDocument:sender];
138	}
139}
140
141- (IBAction)saveDocumentAs:(id)sender
142{
143	if (!canSave) {
144#ifdef __COCOA__
145		NSBeep();
146#endif
147	} else {
148		[super saveDocumentAs:sender];
149	}
150}
151
152- (IBAction)saveDocumentTo:(id)sender
153{
154	if (!canSave) {
155#ifdef __COCOA__
156		NSBeep();
157#endif
158	} else {
159		[super saveDocumentTo:sender];
160	}
161}
162
163- (NSString *)displayName
164{
165	if (!canSave) {
166		return @"";
167	}
168	return [super displayName];
169}
170
171- (void)changeDirtyFileFromFilename:(NSString *)from toFilename:(NSString *)to
172{
173	NSMutableArray *dirtyFiles = [[[[NSUserDefaults standardUserDefaults] objectForKey:@"PXDirtyFiles"] mutableCopy] autorelease];
174
175	if (dirtyFiles == nil) {
176		dirtyFiles = [NSMutableArray arrayWithCapacity:8];
177	}
178
179	if ((from != nil) && !NSEqualSizes([canvas size], NSZeroSize)) {
180		if (![[NSFileManager defaultManager] removeFileAtPath:from handler:nil]) {
181			NSLog(@"Could not delete backup file \"%@\"", from);
182		}
183		[dirtyFiles removeObject:from];
184	}
185
186	if (to != nil) {
187		[dirtyFiles addObject:to];
188	}
189
190	[[NSUserDefaults standardUserDefaults] setObject:dirtyFiles forKey:@"PXDirtyFiles"];
191	[[NSUserDefaults standardUserDefaults] synchronize];
192}
193
194- (void)cleanUpAutosaveFile:(NSNotification *)aNotification
195{
196	[self changeDirtyFileFromFilename:autosaveFilename toFilename:nil]; // this will remove our dirty autosave file from the user defaults, so it doesn't complain on the next start of Pixen and freak out the user
197	autosaveFilename = nil;
198}
199
200
201- (void)updateAutosaveFilename
202{
203	NSString *oldFilename = autosaveFilename;
204
205	if ([self fileName] != nil) {
206		autosaveFilename = [[[[[self fileName] stringByDeletingPathExtension] stringByAppendingString:@"~"] stringByAppendingPathExtension:@"pxi"] retain];
207	} else {
208		autosaveFilename = [@"/tmp/PixenAutosave.pxi" retain];
209	}
210	if (![oldFilename isEqualToString:autosaveFilename]) {
211		[self changeDirtyFileFromFilename:oldFilename toFilename:autosaveFilename];
212	}
213
214	[oldFilename release];
215}
216
217- (void)setFileName:(NSString *)path
218{
219	[super setFileName:path];
220	[self autosave:nil];
221}
222
223- (void)autosave:(NSTimer *)timer
224{
225	if (![self rescheduleAutosave]) {
226		return;
227	}
228	[self updateAutosaveFilename];
229	if (canvas && !NSEqualSizes([canvas size], NSZeroSize)) {
230		[[self dataRepresentationOfType:@"Pixen Image"] writeToFile:autosaveFilename atomically:YES];
231	}
232}
233
234- (void)dealloc
235{
236	[autosaveFilename release];
237	[[NSNotificationCenter defaultCenter] removeObserver:self];
238	[autosaveTimer invalidate];
239	[autosaveTimer release];
240    [[self windowControllers] makeObjectsPerformSelector:@selector(close)];
241    [canvasController release];
242    [canvas release];
243    [super dealloc];
244}
245
246- (void)makeWindowControllers
247{
248    // Override returning the nib file name of the document
249    // If you need to use a subclass of NSWindowController or if your document supports multiple NSWindowControllers, you should remove this method and override -makeWindowControllers instead.
250    canvasController = [[PXCanvasController alloc] init];
251    [canvasController setCanvas:canvas];
252    [self addWindowController:canvasController];
253    [canvasController window];
254	[[NSNotificationCenter defaultCenter] postNotificationName:PXDocumentOpened object:self];
255}
256
257- (void)windowControllerDidLoadNib:aController
258{
259    [super windowControllerDidLoadNib:aController];
260    // Add any code here that needs to be executed once the windowController has loaded the document's window.
261}
262
263- (void)close
264{
265	[[NSNotificationCenter defaultCenter] postNotificationName:PXDocumentClosed object:self];
266	[autosaveTimer invalidate];
267	[self cleanUpAutosaveFile:nil];
268	[super close];
269}
270
271BOOL isPowerOfTwo(int num)
272{
273	double logResult = log2(num);
274	return (logResult == (int)logResult);
275}
276
277- (NSData *)dataRepresentationOfType:(NSString *)aType
278{
279	if (!canSave) {
280		return nil;
281	}
282
283	if([aType isEqualToString:@"Pixen Image"])
284    {
285		return [NSKeyedArchiver archivedDataWithRootObject:canvas];
286    }
287	if([aType isEqualToString:@"Portable Network Graphic (PNG)"])
288    {
289		return [canvas imageDataWithType:NSPNGFileType properties:nil];
290    }
291	if([aType isEqualToString:@"Tagged Image File Format (TIFF)"])
292    {
293		return [canvas imageDataWithType:NSTIFFFileType properties:nil];
294    }
295
296#ifdef __COCOA__
297	if([aType isEqualToString:@"Compuserve Graphic (GIF)"])
298    {
299		id image = [[NSImage alloc] initWithSize:[canvas size]];
300		[image lockFocus];
301		[canvas drawRect:NSMakeRect(0,0,[canvas size].width,[canvas size].height) fixBug:YES];
302		[image unlockFocus];
303		return [PXGifExporter gifDataForImage:image];
304    }
305	if([aType isEqualToString:@"Windows Bitmap (BMP)"])
306    {
307		return [canvas imageDataWithType:NSBMPFileType properties:nil];
308    }
309	if([aType isEqualToString:@"Apple PICT Graphic"])
310    {
311		return [canvas PICTData];
312    }
313	if([aType isEqualToString:@"Encapsulated PostScript (EPS)"])
314    {
315		return [[(PXCanvasController *)canvasController view] dataWithEPSInsideRect:[[(PXCanvasController *)canvasController view] frame]];
316    }
317
318	if([aType isEqualToString:@"Game of Life File (LIF)"])
319    {
320		NSMutableString *fileString = [NSMutableString stringWithString:@"#Life 1.05\r\n#D Generated by "];
321		[fileString appendString:[[[NSBundle mainBundle] localizedInfoDictionary] objectForKey:@"CFBundleName"]];
322		[fileString appendString:@" "];
323		[fileString appendString:[[[NSBundle mainBundle] localizedInfoDictionary] objectForKey:@"CFBundleShortVersionString"]];
324		[fileString appendString:@"\r\n#N\r\n#P 0 0\r\n"];
325		NSPoint point;
326		int spaces=0, i;
327		BOOL lineFilled;
328		for (point.y=0; point.y<[canvas size].height; point.y++) {
329			lineFilled = NO;
330			for (point.x=0; point.x<[canvas size].width; point.x++) {
331				if ([[canvas colorAtPoint:point] alphaComponent] > .5) {
332					for (i=0; i<spaces; i++) {
333						[fileString appendString:@"."];
334					}
335					spaces = 0;
336					[fileString appendString:@"*"];
337					lineFilled = YES;
338				} else {
339					spaces++;
340				}
341			}
342			spaces = 0;
343			if (!lineFilled) {
344				[fileString appendString:@"."];
345			}
346			[fileString appendString:@"\r\n"];
347		}
348
349		return [fileString dataUsingEncoding:NSUTF8StringEncoding];
350    }
351#else
352#warning implement that without Quicktime !!
353#endif
354
355	return nil;
356}
357
358- (BOOL)checkSize:(NSSize)size
359{
360	if (size.width * size.height <= 256 * 256) {
361		return YES;
362	}
363#ifdef __COCOA__
364	return [[NSAlert alertWithMessageText:@"Large Image Warning" defaultButton:@"Yes" alternateButton:@"No" otherButton:nil informativeTextWithFormat:@"This image is %d by %d pixels in size, which is large enough that manipulation might be noticably slow.  Pixen is designed for images under 256 by 256 pixels.  Would you still like to open this image?", (int)size.width, (int)size.height] runModal] == NSOKButton;
365#else
366#warning GNUstep TODO
367	return YES;
368#endif
369
370}
371
372
373- (BOOL)loadDataRepresentation:(NSData *)data ofType:(NSString *)aType
374{
375	// Insert code here to read your document from the given data.  You can also choose to override -loadFileWrapperRepresentation:ofType: or -readFromFile:ofType: instead.
376	if([aType isEqualToString:@"Pixen Image"])
377    {
378		PXCanvas *tempCanvas = [NSKeyedUnarchiver unarchiveObjectWithData:data];
379		if (![self checkSize:[tempCanvas size]]) {
380			return NO;
381		}
382		[canvas release];
383		canvas = [tempCanvas retain];
384    }
385	/*	else if([aType isEqualToString:@"Photoshop Graphic (PSD)"])
386	{
387		canvas = [[PXCanvas alloc] initWithPSDData:data];
388	} */
389	else if([aType isEqualToString:@"Game of Life File (LIF)"]) {
390#ifdef __COCOA__
391		NSMutableSet *points = [NSMutableSet set];
392#else
393		NSMutableSet *points = [[NSMutableSet alloc] init];
394#warning GNUstep strange error during compilation
395#endif
396		NSRect rect = NSZeroRect;
397		NSArray *lines = [data getLines];
398		NSPoint startingPoint;
399		NSPoint offset;
400		NSEnumerator *lineEnumerator = [lines objectEnumerator];
401		NSString *line;
402		BOOL firstLine = YES;
403		while ( ( line = [lineEnumerator nextObject] ) ) {
404			if (firstLine) {
405				if (![line isEqualToString:@"#Life 1.05\r"]) {
406#ifdef __COCOA__
407					[[NSAlert alertWithMessageText:@"Invalid file!" defaultButton:@"OK" alternateButton:nil otherButton:nil informativeTextWithFormat:@"Only life v1.05 files are supported.  This is a v%@ life file.", [line substringFromIndex:6]] runModal];
408#else
409#warning GNUstep : TODO
410#endif
411					return NO;
412				}
413				firstLine = NO;
414				continue;
415			}
416
417			if ([line length] <= 0) {
418				continue;
419			}
420
421			NSScanner *lineScanner = [NSScanner scannerWithString:line];
422
423			if ([lineScanner scanString:@"#" intoString:NULL]) {
424				if ([lineScanner scanString:@"P" intoString:NULL]) {
425					if (![lineScanner scanFloat:&startingPoint.x]) {
426						return NO;
427					}
428					if (![lineScanner scanFloat:&startingPoint.y]) {
429						return NO;
430					}
431					offset.y = startingPoint.y;
432				}
433				continue;
434			}
435
436			int i=0;
437			offset.x=startingPoint.x;
438			for (i=0; i<[line length]-1; i++) {
439				if ([line characterAtIndex:i]=='*') {
440					[points addObject:[NSValue valueWithPoint:offset]];
441					rect = NSUnionRect(rect, NSMakeRect(offset.x, offset.y, 1, 1));
442				}
443				offset.x++;
444			}
445			offset.y++;
446		}
447		[canvas setSize:rect.size];
448		NSEnumerator *pointEnumerator = [points objectEnumerator];
449		NSValue *point;
450		NSPoint pointValue;
451		while ( (point = [pointEnumerator nextObject]) ) {
452			pointValue = [point pointValue];
453			pointValue.x -= rect.origin.x;
454			pointValue.y -= rect.origin.y;
455			[[[canvas activeLayer] image] setPixel:[PXPixel withColor:[NSColor blackColor]] atPoint:pointValue]; // so the notification doesn't get sent
456		}
457	}
458	else
459    {
460		NSImage *image = [[[NSImage alloc] initWithData:data] autorelease];
461		if (![self checkSize:[image size]]) {
462			return NO;
463		}
464		[canvas release];
465		canvas = [[PXCanvas alloc] initWithImage:image];
466    }
467	if(canvas)
468    {
469		[canvasController setCanvas:canvas];
470		return YES;
471    }
472	return NO;
473}
474
475- (void)setLayers:layers fromLayers:oldLayers
476{
477	[canvas setLayers:layers fromLayers:oldLayers];
478	//[[[self undoManager] prepareWithInvocationTarget:self] setLayers:oldLayers fromLayers:layers];
479	//[canvas setLayers:layers];
480	//[canvas setSize:[[layers objectAtIndex:0] size]];
481}
482
483- (IBAction)cut:sender
484{
485	[[self undoManager] beginUndoGrouping];
486	[self setLayers:[[canvas layers] deepMutableCopy] fromLayers:[canvas layers]];
487	[self copy:sender];
488	[self delete:sender];
489	[[self undoManager] setActionName:@"Cut"];
490	[[self undoManager] endUndoGrouping];
491	[canvas changedInRect:NSMakeRect(0, 0, [canvas size].width, [canvas size].height)];
492}
493
494- (IBAction)copy:sender
495{
496	id board = [NSPasteboard generalPasteboard];
497	[board declareTypes:[NSArray arrayWithObject:@"PXLayer"] owner:self];
498	if(![[board types] containsObject:@"PXLayer"])
499	{
500		[board addTypes:[NSArray arrayWithObject:@"PXLayer"] owner:self];
501	}
502	[board setData:[canvas selectionData] forType:@"PXLayer"];
503}
504
505- (IBAction)paste:sender
506{
507	[[self undoManager] beginUndoGrouping];
508	[[self undoManager] setActionName:@"Paste"];
509	[self setLayers:[[canvas layers] deepMutableCopy] fromLayers:[canvas layers]];
510	[[[self undoManager] prepareWithInvocationTarget:canvasController] canvasSizeDidChange:nil];
511	[[self undoManager] endUndoGrouping];
512	id board = [NSPasteboard generalPasteboard];
513	if([[board types] containsObject:@"PXLayer"])
514	{
515		[canvas pasteFromPasteboard:board type:@"PXLayer"];
516	}
517	id enumerator = [[NSImage imagePasteboardTypes] objectEnumerator], current;
518	while (( current = [enumerator nextObject] ) )
519	{
520		if ([[board types] containsObject:current])
521		{
522			[canvas pasteFromPasteboard:board type:@"NSImage"];
523		}
524	}
525	[canvas changedInRect:NSMakeRect(0, 0, [canvas size].width, [canvas size].height)];
526}
527
528- (IBAction)delete:sender
529{
530	if (![canvas hasSelection]) { return; }
531	[[self undoManager] beginUndoGrouping];
532	[[self undoManager] setActionName:@"Delete"];
533	[self setLayers:[[canvas layers] deepMutableCopy] fromLayers:[canvas layers]];
534	[[self undoManager] endUndoGrouping];
535	[canvas deleteSelection];
536	[canvas changedInRect:NSMakeRect(0, 0, [canvas size].width, [canvas size].height)];
537}
538
539- (IBAction)selectAll:sender
540{
541	[canvas selectAll];
542	[canvas changedInRect:NSMakeRect(0, 0, [canvas size].width, [canvas size].height)];
543}
544
545- (IBAction)selectNone:sender
546{
547	[[self undoManager] beginUndoGrouping];
548	[[self undoManager] setActionName:@"Deselect"];
549	[self setLayers:[[canvas layers] deepMutableCopy] fromLayers:[canvas layers]];
550	[[self undoManager] endUndoGrouping];
551	[canvas deselect];
552	[canvas changedInRect:NSMakeRect(0, 0, [canvas size].width, [canvas size].height)];
553}
554
555- (void)printShowingPrintPanel:(BOOL)showPanels
556{
557	if(printableView == nil) { printableView = [[PXCanvasPrintView viewForCanvas:[self canvas]] retain]; }
558
559	float scale = [[[[self printInfo] dictionary] objectForKey:NSPrintScalingFactor] floatValue];
560	id transform = [NSAffineTransform transform];
561	[transform scaleXBy:scale yBy:scale];
562	[printableView setBoundsOrigin:[transform transformPoint:[printableView frame].origin]];
563	[printableView setBoundsSize:[transform transformSize:[printableView frame].size]];
564
565    NSPrintOperation *op = [NSPrintOperation printOperationWithView:printableView printInfo:[self printInfo]];
566    [op setShowPanels:showPanels];
567
568#ifdef __COCOA__
569	[self runModalPrintOperation:op delegate:nil didRunSelector:NULL contextInfo:NULL];
570#else
571#warning GNUstep TODO
572#endif
573}
574
575- canvas
576{
577	return canvas;
578}
579
580@end
581