// // ZoomiFictionController.m // ZoomCocoa // // Created by Andrew Hunter on Thu Jan 22 2004. // Copyright (c) 2004 Andrew Hunter. All rights reserved. // // Incorporates changes contributed by Collin Pieper #import #import "ZoomiFictionController.h" #import "ZoomStoryOrganiser.h" #import "ZoomStory.h" #import "ZoomStoryID.h" #import "ZoomAppDelegate.h" #import "ZoomGameInfoController.h" #import "ZoomNotesController.h" #import "ZoomClient.h" #import "ZoomSavePreviewView.h" #import "ZoomRatingCell.h" #import "ZoomHQImageView.h" #import "ZoomPlugInManager.h" #import "ZoomPlugInController.h" #import "ZoomPlugIn.h" #import "ZoomWindowThatIsKey.h" #import "ZoomClearView.h" #import "ZoomSignPost.h" #import "ifmetabase.h" #ifndef NSAppKitVersionNumber10_2 # define NSAppKitVersionNumber10_2 663 #endif @interface ZoomiFictionController(PrivateMethods) - (NSString*) queryEncode: (NSString*) string; - (void) installSignpostPluginFrom: (NSData*) signpostXml; - (void) installPluginFrom: (ZoomDownload*) downloadedPlugin; - (void) updateBackForwardButtons; @end @implementation ZoomiFictionController static ZoomiFictionController* sharedController = nil; static NSString* addDirectory = @"ZoomiFictionControllerDefaultDirectory"; static NSString* sortGroup = @"ZoomiFictionControllerSortGroup"; static NSString* ZoomFieldAttribute = @"ZoomFieldAttribute"; static NSString* ZoomRowAttribute = @"ZoomRowAttribute"; static NSString* ZoomStoryAttribute = @"ZoomStoryAttribute"; enum { ZoomNoField, ZoomTitleField, ZoomYearField, ZoomDescriptionField, ZoomTitleNewlineField, ZoomYearNewlineField, ZoomDescriptionNewlineField }; // = Setup/initialisation = + (ZoomiFictionController*) sharediFictionController { if (!sharedController) { NSString* nibName = @"iFiction"; if (floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_2) nibName = @"iFiction-10.2"; sharedController = [[ZoomiFictionController alloc] initWithWindowNibName: nibName]; } return sharedController; } + (void) initialize { // Create user defaults NSString* docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex: 0]; [[NSUserDefaults standardUserDefaults] registerDefaults: [NSDictionary dictionaryWithObjectsAndKeys: docDir, addDirectory, @"group", sortGroup, nil]]; } - (void) dealloc { [mainView release]; [storyList release]; [sortColumn release]; [filterSet1 release]; [filterSet2 release]; [previewView release]; [resourceDrop release]; [[NSNotificationCenter defaultCenter] removeObserver: self]; [super dealloc]; } // Bug in weak linking? Can't use NSShadowAttributeName... Hmph static NSString* ZoomNSShadowAttributeName = @"NSShadow"; - (NSView*) createMetalTitleForTable: (NSTableView*) theTable { // Jeremy Dronfield suggested this on Cocoa-dev NSRect superRect = [[theTable headerView] frame]; NSRect cornerRect = [[theTable cornerView] frame]; // Allocate the header view NSTableHeaderView* myHeader = [[NSTableHeaderView alloc] initWithFrame:superRect]; [myHeader setAutoresizesSubviews:YES]; // Shadow creates an engraved look NSObject *shadow = [[objc_lookUpClass("NSShadow") alloc] init]; [(id)shadow setShadowOffset:NSMakeSize(1.1, -1.5)]; [(id)shadow setShadowBlurRadius:0.2]; [(id)shadow setShadowColor:[NSColor colorWithCalibratedWhite:1.0 alpha:0.6]]; // The title text NSMutableAttributedString *headerString = [[NSMutableAttributedString alloc] initWithString:@"Title"]; NSRange range = NSMakeRange(0, [headerString length]); [headerString addAttribute:NSFontAttributeName value:[NSFont systemFontOfSize:11.0] range:range]; [headerString addAttribute:ZoomNSShadowAttributeName value:shadow range:range]; [headerString setAlignment:NSCenterTextAlignment range:range]; // The background image NSImageView *imageView = [[NSImageView alloc] initWithFrame:superRect]; [imageView setImageFrameStyle:NSImageFrameNone]; [imageView setImageAlignment:NSImageAlignCenter]; [imageView setImageScaling:NSScaleToFit]; [imageView setImage:[NSImage imageNamed:@"Metal-Title"]]; [imageView setAutoresizingMask:NSViewWidthSizable]; // Set the corner view image NSImageView *cornerImage = [[NSImageView alloc] initWithFrame:cornerRect]; [cornerImage setImageFrameStyle:NSImageFrameNone]; [cornerImage setImageAlignment:NSImageAlignCenter]; [cornerImage setImageScaling:NSScaleToFit]; [cornerImage setImage:[NSImage imageNamed:@"Metal-Title"]]; // The header label NSTextField *headerText = [[NSTextField alloc] initWithFrame:superRect]; [headerText setAutoresizingMask:NSViewWidthSizable]; [headerText setDrawsBackground:NO]; [headerText setBordered:NO]; [headerText setEditable:NO]; [headerText setAttributedStringValue:headerString]; [myHeader addSubview:imageView]; [myHeader addSubview:headerText]; [theTable setHeaderView:myHeader]; [theTable setCornerView:cornerImage]; // The menu [myHeader setMenu: [theTable menu]]; [headerText setMenu: [theTable menu]]; [imageView setMenu: [theTable menu]]; [headerText release]; [imageView release]; [cornerImage release]; [headerString release]; [shadow release]; return myHeader; } - (void) setTitle: (NSString*) title forTable: (NSTableView*) table { // Can't use the traditional methods, as our table header view draws all over the normal // column header if (objc_lookUpClass("NSShadow") != nil) { NSTableHeaderView* theHeader = [table headerView]; NSEnumerator* viewEnum = [[theHeader subviews] objectEnumerator]; NSTextField* titleView; // Shadow creates an engraved look NSObject *shadow = [[objc_lookUpClass("NSShadow") alloc] init]; [shadow setShadowOffset:NSMakeSize(1.1, -1.5)]; [shadow setShadowBlurRadius:0.2]; [shadow setShadowColor:[NSColor colorWithCalibratedWhite:1.0 alpha:0.6]]; // The title text NSMutableAttributedString *headerString = [[NSMutableAttributedString alloc] initWithString: title]; NSRange range = NSMakeRange(0, [headerString length]); [headerString addAttribute:NSFontAttributeName value:[NSFont systemFontOfSize:11.0] range:range]; [headerString addAttribute:ZoomNSShadowAttributeName value:shadow range:range]; [headerString setAlignment:NSCenterTextAlignment range:range]; while (titleView = [viewEnum nextObject]) { if ([titleView isKindOfClass: [NSTextField class]]) { [titleView setAttributedStringValue: headerString]; } } [headerString release]; [shadow release]; } else { NSTableColumn* filterColumn = [[table tableColumns] objectAtIndex: 0]; [[filterColumn headerCell] setStringValue: title]; } } - (void) flipTo: (NSView*) view { NSRect viewFrame = [topPanelView bounds]; viewFrame.origin = NSMakePoint(0,0); [view setFrame: viewFrame]; [flipView prepareToAnimateView: topPanelView]; [flipView animateTo: view style: ZoomAnimateFade]; if (view == filterView) { [flipButtonMatrix selectCellWithTag: 2]; } else if (view == infoView) { [flipButtonMatrix selectCellWithTag: 1]; } else if (view == saveGameView) { [flipButtonMatrix selectCellWithTag: 0]; } topPanelView = view; [flipButtonMatrix setNextKeyView: view]; [view setNextKeyView: mainTableView]; } - (void) positionDownloadWindow { if (![downloadWindow isVisible]) return; NSRect mainWindowFrame = [[self window] frame]; NSRect downloadFrame = [downloadWindow frame]; downloadFrame.origin.x = NSMaxX(mainWindowFrame) - downloadFrame.size.width - 9; downloadFrame.origin.y = NSMinY(mainWindowFrame) + 42; [downloadWindow setFrameOrigin: downloadFrame.origin]; } - (void) windowDidLoad { [mainView retain]; [ifdbView setFrameLoadDelegate: self]; [ifdbView setPolicyDelegate: self]; #ifdef DEVELOPMENT_BUILD [ifdbView setCustomUserAgent: @"Mozilla/5.0 (Macintosh; U; Mac OS X; en-us) AppleWebKit (KHTML like Gecko) uk.org.logicalshift.zoom/1.1.2/development"]; #else [ifdbView setCustomUserAgent: @"Mozilla/5.0 (Macintosh; U; Mac OS X; en-us) AppleWebKit (KHTML like Gecko) uk.org.logicalshift.zoom/1.1.2/release"]; #endif NSURL* loadingPage = [NSURL fileURLWithPath: [[NSBundle mainBundle] pathForResource: @"ifdb-loading" ofType: @"html"]]; [[ifdbView mainFrame] loadRequest: [NSURLRequest requestWithURL: loadingPage]]; NSView* clearView = [[[ZoomClearView alloc] init] autorelease]; downloadView = [[ZoomDownloadView alloc] initWithFrame: NSMakeRect(0,0,276,78)]; downloadWindow = [[ZoomWindowThatIsKey alloc] initWithContentRect: NSMakeRect(0,0,276,78) styleMask: NSBorderlessWindowMask backing: NSBackingStoreBuffered defer: NO]; [downloadWindow setOpaque: NO]; [downloadWindow setAlphaValue: 0]; [downloadWindow setContentView: clearView]; [downloadView setFrame: [clearView frame]]; [clearView addSubview: downloadView]; [addButton setPushedImage: [NSImage imageNamed: @"add-in"]]; [newgameButton setPushedImage: [NSImage imageNamed: @"newgame-in"]]; [continueButton setPushedImage: [NSImage imageNamed: @"continue-in"]]; [infoButton setPushedImage: [NSImage imageNamed: @"information-in"]]; [continueButton setEnabled: NO]; [newgameButton setEnabled: NO]; [[self window] setFrameUsingName: @"iFiction"]; [[self window] setExcludedFromWindowsMenu: YES]; [gameDetailView setTextContainerInset: NSMakeSize(6.0, 6.0)]; [[gameDetailView textStorage] setDelegate: self]; [self setupSplitView]; // Set up the filter table headers (panther only) if (objc_lookUpClass("NSShadow") != nil) { // We have NSShadow - go ahead // Note (and FIXME): a retained view is not released here // (Being lazy: this doesn't matter, as the iFiction window is persistent) [self createMetalTitleForTable: filterTable1]; [self createMetalTitleForTable: filterTable2]; [self setTitle: @"Group" forTable: filterTable1]; [self setTitle: @"Author" forTable: filterTable2]; } showDrawer = YES; needsUpdating = YES; if (sortColumn) [sortColumn release]; sortColumn = [[[NSUserDefaults standardUserDefaults] objectForKey: sortGroup] copy]; [mainTableView setHighlightedTableColumn:[mainTableView tableColumnWithIdentifier:sortColumn]]; [mainTableView setAllowsColumnSelection: NO]; // Add a 'ratings' column to the main table NSTableColumn* newColumn = [[NSTableColumn alloc] initWithIdentifier: @"rating"]; [newColumn setDataCell: [[[ZoomRatingCell alloc] init] autorelease]]; [newColumn setMinWidth: 84]; [newColumn setMaxWidth: 84]; [newColumn setEditable: YES]; [[newColumn headerCell] setStringValue: @"Rating"]; [mainTableView addTableColumn: [newColumn autorelease]]; // Turn on autosaving [mainTableView setAutosaveName: @"ZoomStoryTable"]; [mainTableView setAutosaveTableColumns: YES]; // Update the table when the story list changes [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(storyListChanged:) name: ZoomStoryOrganiserChangedNotification object: [ZoomStoryOrganiser sharedStoryOrganiser]]; // Deal with progress indicator notifications [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(storyProgressChanged:) name: ZoomStoryOrganiserProgressNotification object: [ZoomStoryOrganiser sharedStoryOrganiser]]; [self configureFromMainTableSelection]; [mainTableView reloadData]; [mainTableView registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType, nil]]; [mainTableView setDoubleAction:@selector(startNewGame:)]; [self flipTo: filterView]; } - (void) close { [[self window] orderOut: self]; [[self window] saveFrameUsingName: @"iFiction"]; } - (void)windowDidMove:(NSNotification *)aNotification { [[self window] saveFrameUsingName: @"iFiction"]; } - (void) setBrowserFontSize { NSRect viewFrame = [browserView frame]; BOOL shouldUseSmallFonts = NO; if (viewFrame.size.width < 900 || viewFrame.size.height < 400) { shouldUseSmallFonts = YES; } if (shouldUseSmallFonts != smallBrowser) { smallBrowser = shouldUseSmallFonts; [ifdbView setTextSizeMultiplier: shouldUseSmallFonts?0.7:1.0]; } } - (void)windowDidResize:(NSNotification *)notification { [[self window] saveFrameUsingName: @"iFiction"]; if (browserOn) [self setBrowserFontSize]; [self positionDownloadWindow]; } // = Useful functions for getting info about the table = - (ZoomStoryID*) selectedStoryID { if (needsUpdating) [self reloadTableData]; if ([mainTableView numberOfSelectedRows] == 1) { ZoomStoryID* ident = [storyList objectAtIndex: [mainTableView selectedRow]]; return ident; } return nil; } - (ZoomStory*) selectedStory { ZoomStoryID* ident = [self selectedStoryID]; if (ident != nil) { return [self storyForID: ident]; } return nil; } - (NSString*) selectedFilename { ZoomStoryID* ident = [self selectedStoryID]; if (ident != nil) { return [[ZoomStoryOrganiser sharedStoryOrganiser] filenameForIdent: ident]; } return nil; } - (ZoomStory*) createStoryCopy: (ZoomStory*) theStory { if (theStory == nil) { return nil; } [[[NSApp delegate] userMetadata] copyStory: theStory]; return [[[NSApp delegate] userMetadata] findOrCreateStory: [theStory storyID]]; } // = Panel actions = - (void) addFilesFromPanel: (NSOpenPanel *)sheet returnCode: (int)returnCode contextInfo: (void *)contextInfo { if (returnCode != NSOKButton) return; // Store the defaults [[NSUserDefaults standardUserDefaults] setObject: [sheet directory] forKey: addDirectory]; NSArray * filenames = [sheet filenames]; [self addFiles:filenames]; } - (void) addFiles: (NSArray *)filenames { NSArray* fileTypes = [NSArray arrayWithObjects: @"z3", @"z4", @"z5", @"z6", @"z7", @"z8", @"blorb", @"zblorb", @"blb", @"zlb", nil]; // Add all the files we can NSMutableArray* selectedFiles = [filenames mutableCopy]; NSString* filename; while( [selectedFiles count] > 0 ) { NSAutoreleasePool* p = [[NSAutoreleasePool alloc] init]; BOOL isDir; filename = [selectedFiles objectAtIndex:0]; isDir = NO; [[NSFileManager defaultManager] fileExistsAtPath: filename isDirectory: &isDir]; NSString* fileType = [filename pathExtension]; Class plugin; if (isDir) { NSArray* dirContents = [[NSFileManager defaultManager] directoryContentsAtPath: filename]; NSEnumerator* dirContentsEnum = [dirContents objectEnumerator]; NSString* dirComponent; while (dirComponent = [dirContentsEnum nextObject]) { [selectedFiles addObject: [filename stringByAppendingPathComponent: dirComponent]]; } } else if ( [fileTypes containsObject: fileType] ) { ZoomStoryID* fileID = [[ZoomStoryID alloc] initWithZCodeFile: filename]; if (fileID != nil) { [[ZoomStoryOrganiser sharedStoryOrganiser] addStory: filename withIdent: fileID organise: [[ZoomPreferences globalPreferences] keepGamesOrganised]]; [fileID release]; } } else if (plugin = [[ZoomPlugInManager sharedPlugInManager] plugInForFile: filename]) { ZoomPlugIn* instance = [[[plugin alloc] initWithFilename: filename] autorelease]; ZoomStoryID* fileID = [instance idForStory]; if (fileID != nil) { [[ZoomStoryOrganiser sharedStoryOrganiser] addStory: filename withIdent: fileID organise: [[ZoomPreferences globalPreferences] keepGamesOrganised]]; } } [selectedFiles removeObjectAtIndex:0]; [p release]; } [selectedFiles release]; } // = IB actions = - (BOOL) panel:(id)sender shouldShowFilename:(NSString *)filename { BOOL exists; BOOL isDirectory; exists = [[NSFileManager defaultManager] fileExistsAtPath: filename isDirectory: &isDirectory]; if (!exists) return NO; // Show directories that are not packages if (isDirectory) { if ([[NSWorkspace sharedWorkspace] isFilePackageAtPath: filename]) { return NO; } else { return YES; } } // Don't show non-readable files if (![[NSFileManager defaultManager] isReadableFileAtPath: filename]) { return NO; } // Show files that have a valid plugin Class pluginClass = [[ZoomPlugInManager sharedPlugInManager] plugInForFile: filename]; if (pluginClass != nil) { return YES; } // Show files that we can open with the ZoomClient document type NSArray* extensions = [[NSDocumentController sharedDocumentController] fileExtensionsFromType: @"ZCode story"]; NSEnumerator* extnEnum = [extensions objectEnumerator]; NSString* extn; NSString* fileExtension = [[filename pathExtension] lowercaseString]; while (extn = [extnEnum nextObject]) { if ([extn isEqualToString: fileExtension]) return YES; } extensions = [NSArray arrayWithObjects: @"zblorb", @"zlb", nil]; extnEnum = [extensions objectEnumerator]; while (extn = [extnEnum nextObject]) { if ([extn isEqualToString: fileExtension]) return YES; } extensions = [[NSDocumentController sharedDocumentController] fileExtensionsFromType: @"Blorb resource file"]; extnEnum = [extensions objectEnumerator]; while (extn = [extnEnum nextObject]) { if ([extn isEqualToString: fileExtension]) return YES; } return NO; } - (BOOL) panel:(id)sender isValidFilename:(NSString *)filename { if (![self panel: sender shouldShowFilename: filename]) return NO; BOOL exists; BOOL isDirectory; exists = [[NSFileManager defaultManager] fileExistsAtPath: filename isDirectory: &isDirectory]; if (!exists) return NO; if (isDirectory) return YES; return YES; } - (IBAction) addButtonPressed: (id) sender { // Create an open panel NSOpenPanel* storiesToAdd; NSArray* fileTypes = [NSArray arrayWithObjects: @"z3", @"z4", @"z5", @"z6", @"z7", @"z8", nil]; storiesToAdd = [NSOpenPanel openPanel]; [storiesToAdd setAllowsMultipleSelection: YES]; [storiesToAdd setCanChooseDirectories: YES]; [storiesToAdd setCanChooseFiles: YES]; [storiesToAdd setDelegate: self]; NSString* path = [[NSUserDefaults standardUserDefaults] objectForKey: addDirectory]; [storiesToAdd beginSheetForDirectory: path file: nil types: nil modalForWindow: [self window] modalDelegate: self didEndSelector: @selector(addFilesFromPanel:returnCode:contextInfo:) contextInfo: nil]; } - (void) autosaveAlertFinished: (NSWindow *)alert returnCode: (int)returnCode contextInfo: (void *)contextInfo { if (returnCode == NSAlertAlternateReturn) { NSString* filename = [self selectedFilename]; if (![[NSFileManager defaultManager] fileExistsAtPath: filename]) { NSLog(@"Couldn't find anything at %@ (looking for IFID: %@)", filename, [self selectedStoryID]); NSBeginAlertSheet(@"Zoom cannot find this story", @"Cancel", nil, nil, [self window], nil, nil, nil, nil, [NSString stringWithFormat: @"Zoom was expecting to find the story file for %@ at %@, but it is no longer there. You will need to locate the story in the Finder and load it manually.", [[self selectedStory] title], filename]); return; } // FIXME: multiple selections? if (filename) { [[NSApp delegate] application: NSApp openFile: filename]; /* [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfFile: filename display: YES]; */ [self configureFromMainTableSelection]; } } } - (IBAction) startNewGame: (id) sender { ZoomStoryID* ident = [self selectedStoryID]; if( ident == NULL ) return; // If an autosave file exists, query the user NSString* autosaveDir = [[ZoomStoryOrganiser sharedStoryOrganiser] directoryForIdent: ident create: NO]; NSString* autosaveFile = [autosaveDir stringByAppendingPathComponent: @"autosave.zoomauto"]; if ([[NSFileManager defaultManager] fileExistsAtPath: autosaveFile]) { // Autosave file exists - show alert sheet NSBeginAlertSheet(@"An autosave file exists for this game", @"Don't start new game", @"Start new game", nil, [self window], self, @selector(autosaveAlertFinished:returnCode:contextInfo:), nil,nil, @"This game has an autosave file associated with it. Starting a new game will cause this file to be overwritten."); } else { // Fake alert sheet OK [self autosaveAlertFinished: nil returnCode: NSAlertAlternateReturn contextInfo: nil]; } } - (IBAction) restoreAutosave: (id) sender { NSString* filename = [self selectedFilename]; // FIXME: multiple selections?, actually save/restore autosaves if (filename) { ZoomClient* newDoc = [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfFile: filename display: NO]; if ([[newDoc windowControllers] count] == 0) { [newDoc makeWindowControllers]; [newDoc loadDefaultAutosave]; } [[[newDoc windowControllers] objectAtIndex: 0] showWindow: self]; [self configureFromMainTableSelection]; } } - (IBAction) searchFieldChanged: (id) sender { if (browserOn) { } else { [self reloadTableData]; [mainTableView reloadData]; } } - (IBAction) changeFilter1: (id) sender { NSString* filterName = [ZoomStory keyForTag: [sender tag]]; NSString* filterTitle = [ZoomStory nameForKey: filterName]; if (!filterName || !filterTitle) { return; } NSTableColumn* filterColumn = [[filterTable1 tableColumns] objectAtIndex: 0]; [filterColumn setIdentifier: filterName]; [self setTitle: filterTitle forTable: filterTable1]; //[[filterColumn headerCell] setStringValue: filterTitle]; [filterTable1 selectRow: 0 byExtendingSelection: NO]; [filterTable2 selectRow: 0 byExtendingSelection: NO]; [self reloadTableData]; [mainTableView reloadData]; } - (IBAction) changeFilter2: (id) sender { NSString* filterName = [ZoomStory keyForTag: [sender tag]]; NSString* filterTitle = [ZoomStory nameForKey: filterName]; if (!filterName || !filterTitle) { return; } NSTableColumn* filterColumn = [[filterTable2 tableColumns] objectAtIndex: 0]; [filterColumn setIdentifier: filterName]; [self setTitle: filterTitle forTable: filterTable2]; //[[filterColumn headerCell] setStringValue: filterTitle]; [filterTable2 selectRow: 0 byExtendingSelection: NO]; [self reloadTableData]; [mainTableView reloadData]; } // = Notifications = - (void) queueStoryUpdate { // Queues an update to run next time through the run loop if (!queuedUpdate) { [[NSRunLoop currentRunLoop] performSelector: @selector(finishUpdatingStoryList:) target: self argument: self order: 128 modes: [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSModalPanelRunLoopMode, nil]]; queuedUpdate = YES; } } - (void) finishUpdatingStoryList: (id) sender { queuedUpdate = NO; [mainTableView reloadData]; [self configureFromMainTableSelection]; } - (void) storyListChanged: (NSNotification*) not { needsUpdating = YES; [self queueStoryUpdate]; //[self finishUpdatingStoryList: self]; } - (void) storyProgressChanged: (NSNotification*) not { NSDictionary* userInfo = [not userInfo]; BOOL activated = [[userInfo objectForKey: @"ActionStarting"] boolValue]; if (activated) { indicatorCount++; } else { indicatorCount--; } if (indicatorCount <= 0) { indicatorCount = 0; [progressIndicator stopAnimation: self]; } else { [progressIndicator startAnimation: self]; } } - (void)windowDidBecomeMain:(NSNotification *)aNotification { [[ZoomGameInfoController sharedGameInfoController] setInfoOwner: self]; [[ZoomNotesController sharedNotesController] setInfoOwner: self]; [self configureFromMainTableSelection]; } - (void)windowDidResignMain:(NSNotification *)aNotification { if ([[ZoomGameInfoController sharedGameInfoController] infoOwner] == self) { [[ZoomGameInfoController sharedGameInfoController] setGameInfo: nil]; [[ZoomGameInfoController sharedGameInfoController] setInfoOwner: nil]; } if ([[ZoomNotesController sharedNotesController] infoOwner] == self) { [[ZoomNotesController sharedNotesController] setGameInfo: nil]; [[ZoomNotesController sharedNotesController] setInfoOwner: nil]; } } // = Our life as a data source = - (ZoomStory*) storyForID: (ZoomStoryID*) ident { ZoomStoryOrganiser* org = [ZoomStoryOrganiser sharedStoryOrganiser]; NSString* filename = [org filenameForIdent: ident]; ZoomStory* story = [[NSApp delegate] findStory: ident]; if (filename == nil) filename = @"No filename"; if (story == nil) { Class pluginClass = [[ZoomPlugInManager sharedPlugInManager] plugInForFile: filename]; ZoomPlugIn* pluginInstance = pluginClass?[[pluginClass alloc] initWithFilename: filename]:nil; if (pluginInstance) { story = [[pluginInstance autorelease] defaultMetadata]; } else { story = [ZoomStory defaultMetadataForFile: filename]; } // Store this in the user metadata for later NSLog(@"Failed to find story for ID: %@", ident); if (story != nil) { [[[NSApp delegate] userMetadata] copyStory: story]; [[[NSApp delegate] userMetadata] writeToDefaultFile]; story = [[[NSApp delegate] userMetadata] findOrCreateStory: [story storyID]]; } } return story; } - (int) compareRow: (ZoomStoryID*) a withRow: (ZoomStoryID*) b { ZoomStory* sA = [self storyForID: a]; ZoomStory* sB = [self storyForID: b]; NSString* cA = [sA objectForKey: sortColumn]; NSString* cB = [sB objectForKey: sortColumn]; if ([cA length] != [cB length]) { if ([cA length] == 0) return 1; if ([cB length] == 0) return -1; } int res = [cA caseInsensitiveCompare: cB]; if (res == 0) { return [[sA title] caseInsensitiveCompare: [sB title]]; } else { return res; } } int tableSorter(id a, id b, void* context) { ZoomiFictionController* us = context; return [us compareRow: a withRow: b]; } - (void) sortTableData { if (sortColumn != nil) { [storyList sortUsingFunction: tableSorter context: self]; } } - (BOOL) filterTableDataPass1 { // Filter using the selection from the first filter table NSString* filterKey = [[[filterTable1 tableColumns] objectAtIndex: 0] identifier]; // Get the selected items from the first filter table NSMutableSet* filterFor = [NSMutableSet set]; NSEnumerator* selEnum = [filterTable1 selectedRowEnumerator]; NSNumber* selRow; while (selRow = [selEnum nextObject]) { if ([selRow intValue] == 0) { // All selected - no filtering [filterTable1 selectRow: 0 byExtendingSelection: NO]; return NO; } [filterFor addObject: [filterSet1 objectAtIndex: [selRow intValue]-1]]; } // Remove anything that doesn't match the filter int num; for (num = 0; num < [storyList count]; num++) { ZoomStoryID* ident = [storyList objectAtIndex: num]; ZoomStory* thisStory = [self storyForID: ident]; NSString* storyKey = [thisStory objectForKey: filterKey]; if (![filterFor containsObject: storyKey]) { [storyList removeObjectAtIndex: num]; num--; } } return YES; } - (BOOL) filterTableDataPass2 { // Filter using the selection from the second filter table NSString* filterKey = [[[filterTable2 tableColumns] objectAtIndex: 0] identifier]; // Get the selected items from the first filter table NSMutableSet* filterFor = [NSMutableSet set]; NSEnumerator* selEnum = [filterTable2 selectedRowEnumerator]; NSNumber* selRow; BOOL tableFilter; tableFilter = YES; while (selRow = [selEnum nextObject]) { if ([selRow intValue] == 0) { // All selected - no filtering [filterTable2 selectRow: 0 byExtendingSelection: NO]; tableFilter = NO; break; } [filterFor addObject: [filterSet2 objectAtIndex: [selRow intValue]-1]]; } // Remove anything that doesn't match the filter (second filter table *or* the search field) int num; NSString* searchText = [searchField stringValue]; if (!tableFilter && [searchText length] <= 0) return NO; // Nothing to do for (num = 0; num < [storyList count]; num++) { ZoomStoryID* ident = [storyList objectAtIndex: num]; ZoomStory* thisStory = [self storyForID: ident]; // Filter table NSString* storyKey = [thisStory objectForKey: filterKey]; if (tableFilter && ![filterFor containsObject: storyKey]) { [storyList removeObjectAtIndex: num]; num--; continue; } // Search field if ([searchText length] > 0) { if (![thisStory containsText: searchText]) { [storyList removeObjectAtIndex: num]; num--; continue; } } } return tableFilter; } - (void) filterTableData { [self filterTableDataPass1]; [self filterTableDataPass2]; } - (void) reloadTableData { ZoomStoryOrganiser* org = [ZoomStoryOrganiser sharedStoryOrganiser]; needsUpdating = NO; // Store the previous list of selected IDs NSMutableArray* previousIDs = [NSMutableArray array]; NSEnumerator* selEnum = [mainTableView selectedRowEnumerator]; NSNumber* selRow; while (selRow = [selEnum nextObject]) { [previousIDs addObject: [storyList objectAtIndex: [selRow intValue]]]; } // Free up the previous table data [storyList release]; storyList = [[NSMutableArray alloc] init]; [filterSet1 release]; [filterSet2 release]; filterSet1 = [[NSMutableArray alloc] init]; filterSet2 = [[NSMutableArray alloc] init]; // Repopulate the table NSEnumerator* identEnum = [[org storyIdents] objectEnumerator]; ZoomStoryID* ident; NSString* filterKey1 = [[[filterTable1 tableColumns] objectAtIndex: 0] identifier]; NSString* filterKey2 = [[[filterTable2 tableColumns] objectAtIndex: 0] identifier]; while (ident = [identEnum nextObject]) { ZoomStory* thisStory = [self storyForID: ident]; [storyList addObject: ident]; NSString* filterItem1 = [thisStory objectForKey: filterKey1]; if ([filterItem1 length] != 0 && [filterSet1 indexOfObject: filterItem1] == NSNotFound) [filterSet1 addObject: filterItem1]; } // Sort the first filter set [filterSet1 sortUsingSelector: @selector(caseInsensitiveCompare:)]; [filterTable1 reloadData]; // Filter the table as required BOOL wasFiltered = isFiltered; isFiltered = NO; isFiltered = [self filterTableDataPass1] || isFiltered; // Generate + sort the second filter set identEnum = [storyList objectEnumerator]; while (ident = [identEnum nextObject]) { ZoomStory* thisStory = [self storyForID: ident]; NSString* filterItem2 = [thisStory objectForKey: filterKey2]; if ([filterItem2 length] != 0 && [filterSet2 indexOfObject: filterItem2] == NSNotFound) [filterSet2 addObject: filterItem2]; } [filterSet2 sortUsingSelector: @selector(caseInsensitiveCompare:)]; [filterTable2 reloadData]; // Continue filtering isFiltered = [self filterTableDataPass2] || isFiltered; // Sort the table as required [self sortTableData]; // Joogle the selection [mainTableView deselectAll: self]; selEnum = [previousIDs objectEnumerator]; ZoomStoryID* selID; while (selID = [selEnum nextObject]) { unsigned index = [storyList indexOfObject: selID]; if (index != NSNotFound) { [mainTableView selectRow: index byExtendingSelection: YES]; } } // Highlight the 'filter' button if some filtering has occurred if (isFiltered != wasFiltered) { // Prepare to animate to the new style of filtering ZoomFlipView* matrixAnimation = [[[ZoomFlipView alloc] init] autorelease]; [matrixAnimation prepareToAnimateView: flipButtonMatrix]; // Get the cell containing the 'filter' button NSButtonCell* filterButtonCell = [flipButtonMatrix cellWithTag: 2]; // Set its text colour to dark red if filtered NSColor* filterColour; if (isFiltered) { filterColour = [NSColor colorWithDeviceRed: 0.7 green: 0 blue: 0 alpha: 1.0]; } else { filterColour = [NSColor blackColor]; } NSMutableAttributedString* filterButtonTitle = [[[filterButtonCell attributedTitle] mutableCopy] autorelease]; [filterButtonTitle addAttributes: [NSDictionary dictionaryWithObjectsAndKeys: filterColour, NSForegroundColorAttributeName, nil] range: NSMakeRange(0, [filterButtonTitle length])]; [filterButtonCell setAttributedTitle: filterButtonTitle]; // Finish the animation [matrixAnimation animateTo: flipButtonMatrix style: ZoomAnimateFade]; } // Tidy up (prevents a dumb infinite loop possibility) [[NSRunLoop currentRunLoop] cancelPerformSelectorsWithTarget: self]; } - (int)numberOfRowsInTableView:(NSTableView *)aTableView { if (needsUpdating) [self reloadTableData]; if (aTableView == mainTableView) { return [storyList count]; } else if (aTableView == filterTable1) { return [filterSet1 count]+1; } else if (aTableView == filterTable2) { return [filterSet2 count]+1; } else { return 0; // Unknown table view } } - (id) tableView: (NSTableView *) aTableView objectValueForTableColumn: (NSTableColumn *) aTableColumn row: (int) rowIndex { // if (needsUpdating) [self reloadTableData]; if (aTableView == mainTableView) { // Retrieve row details NSString* rowID = [aTableColumn identifier]; ZoomStoryID* ident = [storyList objectAtIndex: rowIndex]; ZoomStory* story = [self storyForID: ident]; // Return the value of the appropriate field if ([rowID isEqualToString: @"rating"]) { return [NSNumber numberWithFloat: [story rating]]; } else { return [story objectForKey: rowID]; } } else if (aTableView == filterTable1) { if (rowIndex == 0) return [NSString stringWithFormat: @"All (%i items)", [filterSet1 count]]; return [filterSet1 objectAtIndex: rowIndex-1]; } else if (aTableView == filterTable2) { if (rowIndex == 0) return [NSString stringWithFormat: @"All (%i items)", [filterSet2 count]]; return [filterSet2 objectAtIndex: rowIndex-1]; } else { return nil; // Unknown table view } } - (void) tableView:(NSTableView *)tableView mouseDownInHeaderOfTableColumn:(NSTableColumn *)tableColumn { if (tableView == mainTableView) { NSString* columnID = [tableColumn identifier]; if (sortColumn == nil || ![sortColumn isEqualToString: columnID]) { [mainTableView setHighlightedTableColumn: tableColumn]; [sortColumn release]; sortColumn = [columnID copy]; [[NSUserDefaults standardUserDefaults] setObject: sortColumn forKey: sortGroup]; //[self sortTableData]; [self reloadTableData]; [mainTableView reloadData]; } } } - (void) updateButtons { if (browserOn) { // None of the lower row of buttons work when the IFDB browser is displayed [continueButton setEnabled: NO]; [newgameButton setEnabled: NO]; [addButton setEnabled: NO]; [infoButton setEnabled: NO]; } else { // The add and info buttons always work [addButton setEnabled: YES]; [infoButton setEnabled: YES]; // The other buttons depend on the current selection NSEnumerator* rowEnum = [mainTableView selectedRowEnumerator]; int numSelected = 0; NSNumber* row; [continueButton setEnabled: NO]; [newgameButton setEnabled: NO]; while (row = [rowEnum nextObject]) { numSelected++; ZoomStoryID* ident = [storyList objectAtIndex: [row intValue]]; NSString* filename = [[ZoomStoryOrganiser sharedStoryOrganiser] filenameForIdent: ident]; if ([[NSDocumentController sharedDocumentController] documentForFileName: filename] != nil) { [continueButton setEnabled: YES]; } else { [newgameButton setEnabled: YES]; NSString* autosaveDir = [[ZoomStoryOrganiser sharedStoryOrganiser] directoryForIdent: ident create: NO]; NSString* autosaveFile = [autosaveDir stringByAppendingPathComponent: @"autosave.zoomauto"]; if ([[NSFileManager defaultManager] fileExistsAtPath: autosaveFile]) { // Can restore an autosave [continueButton setEnabled: YES]; } } } } } - (void) configureFromMainTableSelection { ZoomStoryOrganiser* org = [ZoomStoryOrganiser sharedStoryOrganiser]; if (needsUpdating) [self reloadTableData]; [self updateButtons]; int numSelected = [mainTableView numberOfSelectedRows]; NSImage* coverPicture = nil; NSString* comment; NSString* teaser; NSString* description; if (numSelected == 1) { ZoomStoryID* ident = [storyList objectAtIndex: [mainTableView selectedRow]]; ZoomStory* story = [self storyForID: ident]; if ([[self window] isMainWindow] && [[ZoomGameInfoController sharedGameInfoController] infoOwner] == self) { [[ZoomGameInfoController sharedGameInfoController] setGameInfo: story]; } if ([[self window] isMainWindow] && [[ZoomNotesController sharedNotesController] infoOwner] == self) { [[ZoomNotesController sharedNotesController] setGameInfo: story]; } // Set up the comment, teaser and description views comment = [story comment]; teaser = [story teaser]; description = [story description]; // Set up the save preview view NSString* dir = [[ZoomStoryOrganiser sharedStoryOrganiser] directoryForIdent: ident create: NO]; [previewView setDirectoryToUse: [dir stringByAppendingPathComponent: @"Saves"]]; if ([previewView saveGamesAvailable] != saveGamesAvailable) { // Set the 'saves' tab to dark blue if save games are available saveGamesAvailable = [previewView saveGamesAvailable]; // Prepare to animate to the 'saves available' button ZoomFlipView* matrixAnimation = [[[ZoomFlipView alloc] init] autorelease]; [matrixAnimation prepareToAnimateView: flipButtonMatrix]; // Get the cell containing the 'save' button NSButtonCell* filterButtonCell = [flipButtonMatrix cellWithTag: 0]; // Set its text colour NSColor* filterColour; if (saveGamesAvailable) { filterColour = [NSColor colorWithDeviceRed: 0.7 green: 0 blue: 0.3 alpha: 1.0]; } else { filterColour = [NSColor blackColor]; } NSMutableAttributedString* filterButtonTitle = [[[filterButtonCell attributedTitle] mutableCopy] autorelease]; [filterButtonTitle addAttributes: [NSDictionary dictionaryWithObjectsAndKeys: filterColour, NSForegroundColorAttributeName, nil] range: NSMakeRange(0, [filterButtonTitle length])]; [filterButtonCell setAttributedTitle: filterButtonTitle]; // Finish the animation [matrixAnimation animateTo: flipButtonMatrix style: ZoomAnimateFade]; } // Set up the extra blorb resources display [resourceDrop setDroppedFilename: [story objectForKey: @"ResourceFilename"]]; [resourceDrop setEnabled: YES]; // Set up the cover picture NSString* filename = [org filenameForIdent: ident]; ZoomPlugIn* plugin = [[ZoomPlugInManager sharedPlugInManager] instanceForFile: filename]; if (plugin == nil) { // If there's no plugin, try loading the file as a blorb int coverPictureNumber = [story coverPicture]; ZoomBlorbFile* decodedFile = [[ZoomBlorbFile alloc] initWithContentsOfFile: filename]; // Try to retrieve the frontispiece tag (overrides metadata if present) NSData* front = [decodedFile dataForChunkWithType: @"Fspc"]; if (front != nil && [front length] >= 4) { const unsigned char* fpc = [front bytes]; coverPictureNumber = (((int)fpc[0])<<24)|(((int)fpc[1])<<16)|(((int)fpc[2])<<8)|(((int)fpc[3])<<0); } if (coverPictureNumber >= 0) { // Attempt to retrieve the cover picture image if (decodedFile != nil) { NSData* coverPictureData = [decodedFile imageDataWithNumber: coverPictureNumber]; if (coverPictureData) { coverPicture = [[[NSImage alloc] initWithData: coverPictureData] autorelease]; // Sometimes the image size and pixel size do not match up NSImageRep* coverRep = [[coverPicture representations] objectAtIndex: 0]; NSSize pixSize = NSMakeSize([coverRep pixelsWide], [coverRep pixelsHigh]); if (!NSEqualSizes(pixSize, [coverPicture size])) { [coverPicture setScalesWhenResized: YES]; [coverPicture setSize: pixSize]; } } } } [decodedFile release]; } else { coverPicture = [plugin coverImage]; } } else { if ([[self window] isMainWindow] && [[ZoomGameInfoController sharedGameInfoController] infoOwner] == self) { [[ZoomGameInfoController sharedGameInfoController] setGameInfo: nil]; } if ([[self window] isMainWindow] && [[ZoomNotesController sharedNotesController] infoOwner] == self) { [[ZoomNotesController sharedNotesController] setGameInfo: nil]; } comment = @""; teaser = @""; description = @""; [previewView setDirectoryToUse: nil]; [resourceDrop setDroppedFilename: nil]; [resourceDrop setEnabled: NO]; } if (comment == nil) comment = @""; if (teaser == nil) teaser = @""; if (description == nil) description = @""; // Set the game details BOOL flipToDescription = NO; NSMutableAttributedString* gameDetails = [[NSMutableAttributedString alloc] init]; NSFont* titleFont = [NSFont boldSystemFontOfSize: 14]; NSFont* yearFont = [NSFont systemFontOfSize: 10]; NSFont* descFont = [NSFont systemFontOfSize: 11]; if (numSelected >= 1) { // Get the story and ident NSEnumerator* rowEnum = [mainTableView selectedRowEnumerator]; NSNumber* row; BOOL extraNewline = NO; NSAttributedString* newlineString = [[[NSAttributedString alloc] initWithString: @"\n" attributes: [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt: ZoomNoField], ZoomFieldAttribute, [NSNumber numberWithInt: 0], ZoomRowAttribute, nil]] autorelease]; while (row = [rowEnum nextObject]) { ZoomStoryID* ident = [storyList objectAtIndex: [row intValue]]; ZoomStory* story = [self storyForID: ident]; // Append the title NSString* title = [story title]; NSString* extraText; if (title == nil) title = @"Untitled"; if (extraNewline) { [gameDetails appendAttributedString: newlineString]; [gameDetails appendAttributedString: newlineString]; } [gameDetails appendAttributedString: [[[NSAttributedString alloc] initWithString: title attributes: [NSDictionary dictionaryWithObjectsAndKeys: titleFont, NSFontAttributeName, [NSNumber numberWithInt: ZoomTitleField], ZoomFieldAttribute, row, ZoomRowAttribute, story, ZoomStoryAttribute, nil]] autorelease]]; [gameDetails appendAttributedString: [[[NSAttributedString alloc] initWithString: @"\n" attributes: [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt: ZoomTitleNewlineField], ZoomFieldAttribute, row, ZoomRowAttribute, story, ZoomStoryAttribute, nil]] autorelease]]; // Append the year of publication int year = [story year]; if (year > 0) { NSString* yearText = [NSString stringWithFormat: @"%i", year]; [gameDetails appendAttributedString: [[[NSAttributedString alloc] initWithString: yearText attributes: [NSDictionary dictionaryWithObjectsAndKeys: yearFont, NSFontAttributeName, [NSNumber numberWithInt: ZoomYearField], ZoomFieldAttribute, row, ZoomRowAttribute, story, ZoomStoryAttribute, nil]] autorelease]]; [gameDetails appendAttributedString: [[[NSAttributedString alloc] initWithString: @"\n" attributes: [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt: ZoomYearNewlineField], ZoomFieldAttribute, row, ZoomRowAttribute, story, ZoomStoryAttribute, nil]] autorelease]]; } // Append the description NSString* descText = [story description]; if (descText == nil) descText = [story teaser]; if (descText == nil || [descText length] == 0) descText = @""; if (descText != nil) { [gameDetails appendAttributedString: newlineString]; [gameDetails appendAttributedString: [[[NSAttributedString alloc] initWithString: descText attributes: [NSDictionary dictionaryWithObjectsAndKeys: descFont, NSFontAttributeName, [NSNumber numberWithInt: ZoomDescriptionField], ZoomFieldAttribute, row, ZoomRowAttribute, story, ZoomStoryAttribute, nil]] autorelease]]; if ([descText length] > 0) flipToDescription = YES; } extraNewline = YES; // Always flip if the description view is already displayed if (topPanelView == infoView) flipToDescription = YES; } } else { // Note that there are multiple or no games selected NSString* desc = @"Multiple games selected"; if (numSelected == 0) desc = @"No game selected"; [gameDetails appendAttributedString: [[[NSAttributedString alloc] initWithString: desc attributes: [NSDictionary dictionaryWithObjectsAndKeys: descFont, NSFontAttributeName, nil]] autorelease]]; } if (![[gameDetailView string] isEqualToString: [gameDetails string]]) { if (flipToDescription) [flipView prepareToAnimateView: topPanelView]; [[gameDetailView textStorage] setDelegate: nil]; [[gameDetailView textStorage] setAttributedString: gameDetails]; [[gameDetailView textStorage] setDelegate: self]; } else { flipToDescription = NO; } [gameDetails release]; if (coverPicture == nil) { // TODO: set this to a suitable picture for the game format coverPicture = [NSImage imageNamed: @"zoom-app"]; } // Set the cover picture if (coverPicture) { NSSize imageSize = [coverPicture size]; [gameImageView setImage: coverPicture]; // Setup the picture preview window [picturePreviewView setImage: coverPicture]; NSSize previewSize = [coverPicture size]; NSSize screenSize = [[picturePreview screen] frame].size; if (previewSize.width > screenSize.width-128.0) { float ratio = (screenSize.width-128.0)/previewSize.width; previewSize.width = floorf(previewSize.width*ratio); previewSize.height = floorf(previewSize.height*ratio); } if (previewSize.height > screenSize.height-128.0) { float ratio = (screenSize.height-128.0)/previewSize.height; previewSize.width = floorf(previewSize.width*ratio); previewSize.height = floorf(previewSize.height*ratio); } [picturePreview setContentSize: previewSize]; } else { [gameImageView setImage: nil]; [picturePreview orderOut: self]; } // Do no flipping if the iFiction window is not active (prevents apparently mysterious behaviour) if (flipToDescription && ![[self window] isKeyWindow]) flipToDescription = NO; // Flip any views that need flipping if (flipToDescription) { [[gameDetailView layoutManager] setBackgroundLayoutEnabled: NO]; [infoView setFrame: [topPanelView frame]]; [flipView animateTo: infoView style: ZoomAnimateFade]; [flipButtonMatrix selectCellWithTag: 1]; topPanelView = infoView; [[gameDetailView layoutManager] setBackgroundLayoutEnabled: YES]; } } - (void)tableViewSelectionDidChange:(NSNotification *)aNotification { NSTableView* tableView = [aNotification object]; if (tableView == mainTableView) { [self configureFromMainTableSelection]; } else if (tableView == filterTable1 || tableView == filterTable2) { if (tableView == filterTable1) { [filterTable2 selectRow: 0 byExtendingSelection: NO]; } [self reloadTableData]; [mainTableView reloadData]; } else { // Zzzz } } - (void)tableView:(NSTableView *)tableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn*)aTableColumn row:(int)rowIndex { //if (needsUpdating) [self reloadTableData]; if (tableView == mainTableView) { ZoomStoryID* ident = [storyList objectAtIndex: rowIndex]; ZoomStory* story = [self storyForID: ident]; story = [self createStoryCopy: story]; [story setObject: anObject forKey: [aTableColumn identifier]]; } [[[NSApp delegate] userMetadata] writeToDefaultFile]; } - (BOOL)tableView:(NSTableView *)tv writeRows:(NSArray*)rows toPasteboard:(NSPasteboard*)pboard { if(tv != mainTableView ) return NO; if( [rows count] == 0 ) return NO; if( ![[ZoomPreferences globalPreferences] keepGamesOrganised] ) return NO; [mainTableView cancelEditTimer]; NSMutableArray * fileList = [NSMutableArray array]; int i; for( i = 0; i < [rows count]; i++ ) { ZoomStoryID* ident = [storyList objectAtIndex:[[rows objectAtIndex:i] intValue]]; NSString* gamedir = [[ZoomStoryOrganiser sharedStoryOrganiser] directoryForIdent: ident create: NO]; if( gamedir != NULL ) { [fileList addObject:gamedir]; } } [pboard declareTypes:[NSArray arrayWithObject:NSFilenamesPboardType] owner:nil]; [pboard setPropertyList:fileList forType:NSFilenamesPboardType]; return YES; } - (NSDragOperation)tableView:(NSTableView *)tv validateDrop:(id )sender proposedRow:(int)row proposedDropOperation:(NSTableViewDropOperation)op { NSPasteboard * pasteboard = [sender draggingPasteboard]; NSArray * types = [pasteboard types]; if( op == NSTableViewDropOn ) { [tv setDropRow:row dropOperation:NSTableViewDropAbove]; } if( [sender draggingSource] == mainTableView ) { return NSDragOperationNone; } if( [types containsObject:NSFilenamesPboardType] ) { return NSDragOperationCopy; } else { return NSDragOperationNone; } } - (BOOL)tableView:(NSTableView *)tv acceptDrop:(id )sender row:(int)row dropOperation:(NSTableViewDropOperation)op { NSPasteboard * pasteboard = [sender draggingPasteboard]; NSArray * filenames = [pasteboard propertyListForType:NSFilenamesPboardType]; [self addFiles:filenames]; return YES; } #pragma mark - ///////////////////////////////////////////////////////////////////////////// // split view // // setupSplitView // // - (void) setupSplitView { NSNumber * split_view_percent_number = [[NSUserDefaults standardUserDefaults] objectForKey:@"iFictionSplitViewPercentage"]; NSNumber * split_view_collapsed_number = [[NSUserDefaults standardUserDefaults] objectForKey:@"iFictionSplitViewCollapsed"]; if( split_view_percent_number && split_view_collapsed_number ) { splitViewPercentage = [split_view_percent_number floatValue]; splitViewCollapsed = [split_view_collapsed_number boolValue]; if( splitViewCollapsed ) { [splitView resizeSubviewsToPercentage:0.0]; } else { [splitView resizeSubviewsToPercentage:splitViewPercentage]; } } else { splitViewPercentage = [splitView getSplitPercentage]; splitViewCollapsed = NO; } } // splitViewDidResizeSubviews // // - (void)splitViewDidResizeSubviews:(NSNotification *)notification { float pos = [splitView getSplitPercentage]; if( pos == 0.0 ) { [self collapseSplitView]; } } // splitViewMouseDownProcessed // // - (void)splitViewMouseDownProcessed:(NSSplitView *)aSplitView { float pos = [splitView getSplitPercentage]; if( pos > 0.0 ) { splitViewPercentage = pos; splitViewCollapsed = NO; [[NSUserDefaults standardUserDefaults] setObject: [NSNumber numberWithFloat:splitViewPercentage] forKey:@"iFictionSplitViewPercentage"]; [[NSUserDefaults standardUserDefaults] setObject: [NSNumber numberWithBool:splitViewCollapsed] forKey:@"iFictionSplitViewCollapsed"]; } } // splitViewDoubleClickedOnDivider // // - (void)splitViewDoubleClickedOnDivider:(NSSplitView *)aSplitView { float pos = [splitView getSplitPercentage]; if (pos == 0.0) { [splitView resizeSubviewsToPercentage:splitViewPercentage]; } else { [splitView resizeSubviewsToPercentage:0.0]; [self collapseSplitView]; } } // collapseSplitView // // - (void)collapseSplitView { splitViewCollapsed = YES; [[NSUserDefaults standardUserDefaults] setObject: [NSNumber numberWithFloat:splitViewPercentage] forKey:@"iFictionSplitViewPercentage"]; [[NSUserDefaults standardUserDefaults] setObject: [NSNumber numberWithBool:splitViewCollapsed] forKey:@"iFictionSplitViewCollapsed"]; // reset browser selection, since the browser is getting hidden [filterTable2 selectRow: 0 byExtendingSelection: NO]; [filterTable1 selectRow: 0 byExtendingSelection: NO]; [self reloadTableData]; [mainTableView reloadData]; } #pragma mark - - (IBAction) updateGameInfo: (id) sender { [self configureFromMainTableSelection]; } // = GameInfo window actions = - (IBAction) infoNameChanged: (id) sender { if ([self selectedStory] == nil) return; ZoomStory* story = [self createStoryCopy: [self selectedStory]]; [story setTitle: [[ZoomGameInfoController sharedGameInfoController] title]]; [self reloadTableData]; [mainTableView reloadData]; [[[NSApp delegate] userMetadata] writeToDefaultFile]; } - (IBAction) infoHeadlineChanged: (id) sender { if ([self selectedStory] == nil) return; ZoomStory* story = [self createStoryCopy: [self selectedStory]]; [story setHeadline: [[ZoomGameInfoController sharedGameInfoController] headline]]; [self reloadTableData]; [mainTableView reloadData]; [[[NSApp delegate] userMetadata] writeToDefaultFile]; } - (IBAction) infoAuthorChanged: (id) sender { if ([self selectedStory] == nil) return; ZoomStory* story = [self createStoryCopy: [self selectedStory]]; [story setAuthor: [[ZoomGameInfoController sharedGameInfoController] author]]; [self reloadTableData]; [mainTableView reloadData]; [[[NSApp delegate] userMetadata] writeToDefaultFile]; } - (IBAction) infoGenreChanged: (id) sender { if ([self selectedStory] == nil) return; ZoomStory* story = [self createStoryCopy: [self selectedStory]]; [story setGenre: [[ZoomGameInfoController sharedGameInfoController] genre]]; [self reloadTableData]; [mainTableView reloadData]; [[[NSApp delegate] userMetadata] writeToDefaultFile]; } - (IBAction) infoYearChanged: (id) sender { if ([self selectedStory] == nil) return; ZoomStory* story = [self createStoryCopy: [self selectedStory]]; [story setYear: [[ZoomGameInfoController sharedGameInfoController] year]]; [self reloadTableData]; [mainTableView reloadData]; [[[NSApp delegate] userMetadata] writeToDefaultFile]; } - (IBAction) infoGroupChanged: (id) sender { if ([self selectedStory] == nil) return; ZoomStory* story = [self createStoryCopy: [self selectedStory]]; [story setGroup: [[ZoomGameInfoController sharedGameInfoController] group]]; [self reloadTableData]; [mainTableView reloadData]; [[[NSApp delegate] userMetadata] writeToDefaultFile]; } - (IBAction) infoCommentsChanged: (id) sender { if ([self selectedStory] == nil) return; ZoomStory* story = [self createStoryCopy: [self selectedStory]]; [story setComment: [[ZoomGameInfoController sharedGameInfoController] comments]]; [self reloadTableData]; [mainTableView reloadData]; [[[NSApp delegate] userMetadata] writeToDefaultFile]; } - (IBAction) infoTeaserChanged: (id) sender { if ([self selectedStory] == nil) return; ZoomStory* story = [self createStoryCopy: [self selectedStory]]; [story setTeaser: [[ZoomGameInfoController sharedGameInfoController] teaser]]; [self reloadTableData]; [mainTableView reloadData]; [[[NSApp delegate] userMetadata] writeToDefaultFile]; } - (IBAction) infoResourceChanged: (id) sender { if ([self selectedStory] == nil) return; ZoomStory* story = [self createStoryCopy: [self selectedStory]]; // Update the resource path [story setObject: [[ZoomGameInfoController sharedGameInfoController] resourceFilename] forKey: @"ResourceFilename"]; [[[NSApp delegate] userMetadata] writeToDefaultFile]; // Perform organisation if ([[ZoomPreferences globalPreferences] keepGamesOrganised]) { [[ZoomStoryOrganiser sharedStoryOrganiser] organiseStory: [self selectedStory]]; } } - (IBAction) infoZarfRatingChanged: (id) sender { if ([self selectedStory] == nil) return; ZoomStory* story = [self createStoryCopy: [self selectedStory]]; [story setZarfian: [[ZoomGameInfoController sharedGameInfoController] zarfRating]]; [self reloadTableData]; [mainTableView reloadData]; [[[NSApp delegate] userMetadata] writeToDefaultFile]; } - (IBAction) infoMyRatingChanged: (id) sender { if ([self selectedStory] == nil) return; ZoomStory* story = [self createStoryCopy: [self selectedStory]]; [story setRating: [[ZoomGameInfoController sharedGameInfoController] rating]]; [self reloadTableData]; [mainTableView reloadData]; [[[NSApp delegate] userMetadata] writeToDefaultFile]; } // = NSText delegate = - (BOOL) textView:(NSTextView *)aTextView shouldChangeTextInRange:(NSRange)affectedCharRange replacementString:(NSString *)replacementString { if (aTextView == gameDetailView) { // If there are no selected stories, then do not allow any editing if ([mainTableView numberOfSelectedRows] <= 0) return NO; // If we're editing only one row, and we're at the very end of the text, then we're editing the decription, and that's OK if ([mainTableView numberOfSelectedRows] == 1) { if (affectedCharRange.location == [[aTextView textStorage] length]) return YES; } // If we're inserting, then move the affected character range appropriately BOOL newlineEditsValid = NO; if (affectedCharRange.length == 0) { newlineEditsValid = YES; if (affectedCharRange.location > 0 && (affectedCharRange.location == [[aTextView textStorage] length] || [[[aTextView textStorage] string] characterAtIndex: affectedCharRange.location] == '\n')) { affectedCharRange.location--; } affectedCharRange.length = 1; } // Only allow editing if the row and field are consistent across the range NSRange effectiveRange; NSNumber* initialRow = [[[aTextView textStorage] attributesAtIndex: affectedCharRange.location effectiveRange: &effectiveRange] objectForKey: ZoomRowAttribute]; NSNumber* initialField = [[[aTextView textStorage] attributesAtIndex: affectedCharRange.location effectiveRange: &effectiveRange] objectForKey: ZoomFieldAttribute]; NSNumber* finalRow = [[[aTextView textStorage] attributesAtIndex: affectedCharRange.location+affectedCharRange.length-1 effectiveRange: &effectiveRange] objectForKey: ZoomRowAttribute]; NSNumber* finalField = [[[aTextView textStorage] attributesAtIndex: affectedCharRange.location+affectedCharRange.length-1 effectiveRange: &effectiveRange] objectForKey: ZoomFieldAttribute]; if (initialRow != finalRow || initialField != finalField) { return NO; } // The field being edited must not be NoField int field = [initialField intValue]; if (field == ZoomNoField) return NO; // Can't edit newlines if (!newlineEditsValid && (field == ZoomTitleNewlineField || field == ZoomYearNewlineField || field == ZoomDescriptionNewlineField)) { return NO; } // Newlines are only allowed in the descrption, not the title or year if (field != ZoomDescriptionField) { int x; for (x=0; x<[replacementString length]; x++) { unichar thisChar = [replacementString characterAtIndex: x]; if (thisChar == '\n' || thisChar == '\r') return NO; } } // Only numbers are allowed in the year if (field == ZoomYearField) { int x; for (x=0; x<[replacementString length]; x++) { unichar thisChar = [replacementString characterAtIndex: x]; if (thisChar < '0' || thisChar > '9') return NO; } } } // Default to allowing editing return YES; } - (void) updateStoriesFromDetailView { // We assume that the attributes are contiguous. NSTextStorage* storage = [gameDetailView textStorage]; int pos = 0; ZoomStory* lastStory = nil; NSString* title = nil; NSString* year = nil; NSString* description = nil; while (pos < [storage length]) { NSRange attributeRange; ZoomStory* story; NSNumber* field; // Retrieve the story at this position story = [storage attribute: ZoomStoryAttribute atIndex: pos longestEffectiveRange: &attributeRange inRange: NSMakeRange(pos, [storage length]-pos)]; // Retrieve the field at this position field = [storage attribute: ZoomFieldAttribute atIndex: pos longestEffectiveRange: &attributeRange inRange: NSMakeRange(pos, [storage length]-pos)]; // Move pos on to the next position pos = attributeRange.location + attributeRange.length; // Nothing to do if there's no story or field here if (story == nil || field == nil || [field intValue] == ZoomNoField) { continue; } // Get the new attribute value NSString* newAttributeValue = [[storage string] substringWithRange: attributeRange]; // Update the story (we perform all updates at once, to prevent copying causing only the last change to take effect) if (story != lastStory && lastStory != nil) { lastStory = [self createStoryCopy: lastStory]; if (title) [lastStory setTitle: title]; if (year) [lastStory setYear: [year intValue]]; if (description) [lastStory setDescription: description]; title = year = description = nil; lastStory = nil; } lastStory = story; switch ([field intValue]) { case ZoomTitleField: title = newAttributeValue; break; case ZoomYearField: year = newAttributeValue; break; case ZoomDescriptionField: description = newAttributeValue; break; } } // Update the final story if (lastStory) { lastStory = [self createStoryCopy: lastStory]; if (title) [lastStory setTitle: title]; if (year) [lastStory setYear: [year intValue]]; if (description) [lastStory setDescription: description]; } } - (void)controlTextDidEndEditing:(NSNotification *)aNotification { NSControl* textView = [aNotification object]; if (textView == searchField) { // Perform a search based on the text in the ifdb field if (browserOn && [[searchField stringValue] length] > 0) { NSString* ifdbUrl = [[[NSBundle mainBundle] infoDictionary] objectForKey: @"ZoomIfdbUrl"]; if (!ifdbUrl) { ifdbUrl = @"http://ifdb.tads.org/"; } NSString* searchUrl = [NSString stringWithFormat: @"%@search?searchfor=%@", ifdbUrl, [self queryEncode: [searchField stringValue]]]; [[ifdbView mainFrame] loadRequest: [NSURLRequest requestWithURL: [NSURL URLWithString: searchUrl]]]; } } } - (void)textDidEndEditing:(NSNotification *)aNotification { NSControl* textView = [aNotification object]; if (textView == (NSControl*)gameDetailView) { // Update each of the stories in the game detail view [self updateStoriesFromDetailView]; [[[NSApp delegate] userMetadata] writeToDefaultFile]; } else if (textView == searchField) { [self controlTextDidEndEditing: aNotification]; } else { // Mysterious text view of DOOOOM NSLog(@"Unknown text view"); } [self queueStoryUpdate]; } - (void)textStorageWillProcessEditing:(NSNotification *)aNotification { NSTextStorage* storage = [aNotification object]; if (storage == [gameDetailView textStorage]) { // Get the edited range NSRange edited = [storage editedRange]; NSRange affectedCharRange = NSMakeRange(edited.location + edited.length, 0); // Work out the effective row/field/story at this position if (affectedCharRange.location >= [storage length]) affectedCharRange.location--; NSRange effectiveRange; NSNumber* row = [[storage attributesAtIndex: affectedCharRange.location effectiveRange: &effectiveRange] objectForKey: ZoomRowAttribute]; NSNumber* field = [[storage attributesAtIndex: affectedCharRange.location effectiveRange: &effectiveRange] objectForKey: ZoomFieldAttribute]; ZoomStory* story = [[storage attributesAtIndex: affectedCharRange.location effectiveRange: &effectiveRange] objectForKey: ZoomStoryAttribute]; // If field is nil, and we're at the end, then act as if we're editing the description if ([field intValue] == ZoomNoField && edited.location + edited.length == [storage length] && [mainTableView numberOfSelectedRows] == 1) { row = [NSNumber numberWithInt: [mainTableView selectedRow]]; field = [NSNumber numberWithInt: ZoomDescriptionField]; story = [self selectedStory]; } // If we've got any nil values, then give up if (row == nil || field == nil || story == nil || [field intValue] == ZoomNoField) { return; } // Set the attributes appropriately NSFont* titleFont = [NSFont boldSystemFontOfSize: 14]; NSFont* yearFont = [NSFont systemFontOfSize: 10]; NSFont* descFont = [NSFont systemFontOfSize: 11]; NSFont* font; switch ([field intValue]) { case ZoomTitleNewlineField: field = [NSNumber numberWithInt: ZoomTitleField]; case ZoomTitleField: font = titleFont; break; case ZoomYearNewlineField: field = [NSNumber numberWithInt: ZoomYearField]; case ZoomYearField: font = yearFont; break; case ZoomDescriptionNewlineField: field = [NSNumber numberWithInt: ZoomDescriptionField]; default: font = descFont; break; } NSDictionary* attributes = [NSDictionary dictionaryWithObjectsAndKeys: font, NSFontAttributeName, row, ZoomRowAttribute, field, ZoomFieldAttribute, story, ZoomStoryAttribute, nil]; [storage addAttributes: attributes range: edited]; } } // = Various menus = - (BOOL)validateMenuItem:(id )menuItem { SEL sel = [menuItem action]; if (sel == @selector(delete:)) { // Allow only if at least one game is selected return [mainTableView numberOfSelectedRows] > 0; } else if (sel == @selector(revealInFinder:)) { return [mainTableView numberOfSelectedRows] == 1; } else if (sel == @selector(deleteSavegame:)) { // Allow only if at least one savegame is selected NSLog(@"%@", [previewView selectedSaveGame]); return [previewView selectedSaveGame] != nil; } else if (sel == @selector(saveMetadata:)) { return [mainTableView numberOfSelectedRows] > 0; } return YES; } - (IBAction) delete: (id) sender { // Ask for confirmation if ([mainTableView numberOfSelectedRows] <= 0) return; NSString* request = @"Are you sure you want to destroy the spoons?"; if ([mainTableView numberOfSelectedRows] == 1) { request = @"Are you sure you want to delete this game?"; } else { request = @"Are you sure you want to delete these games?"; } // Maybe FIXME: we can display this as a sheet, but we can't display the 'delete save game?' // dialog that way (it appears as a sheet in the drawer. You'd expect a drawer to be a child // window, but it isn't, so there doesn't seem to be a way of retrieving the window to display // under. Well, I can think of a couple of ways around this, but they all feel like ugly hacks) NSEnumerator* rowEnum = [mainTableView selectedRowEnumerator]; NSNumber* row; NSMutableArray* storiesToDelete = [NSMutableArray array]; while (row = [rowEnum nextObject]) { [storiesToDelete addObject: [storyList objectAtIndex: [row intValue]]]; } NSBeginAlertSheet(@"Are you sure?", @"Delete", @"Keep", nil, [self window], self, @selector(confirmDelete:returnCode:contextInfo:), nil, [storiesToDelete retain], request); } - (void) confirmMoveToTrash: (NSWindow *)sheet returnCode: (int)returnCode contextInfo: (void *)contextInfo { NSMutableArray* storiesToDelete = contextInfo; [storiesToDelete autorelease]; if (returnCode != NSAlertDefaultReturn) return; ZoomStoryID* ident; NSEnumerator* rowEnum = [storiesToDelete objectEnumerator]; while (ident = [rowEnum nextObject]) { NSString* filename = [[ZoomStoryOrganiser sharedStoryOrganiser] directoryForIdent: ident create: NO]; if (filename != nil) { int tag; [[NSWorkspace sharedWorkspace] performFileOperation: NSWorkspaceRecycleOperation source: [filename stringByDeletingLastPathComponent] destination: @"" files: [NSArray arrayWithObject: [filename lastPathComponent]] tag: &tag]; // (make sure it's gone from the organiser) [[ZoomStoryOrganiser sharedStoryOrganiser] removeStoryWithIdent: ident deleteFromMetadata: YES]; } } } - (void) confirmDelete:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo { NSMutableArray* storiesToDelete = contextInfo; [storiesToDelete autorelease]; if (returnCode != NSAlertDefaultReturn) return; // Delete the selected games from the organiser ZoomStoryID* ident; NSEnumerator* rowEnum = [storiesToDelete objectEnumerator]; while (ident = [rowEnum nextObject]) { [[ZoomStoryOrganiser sharedStoryOrganiser] removeStoryWithIdent: ident deleteFromMetadata: YES]; } if ([[ZoomPreferences globalPreferences] keepGamesOrganised]) { [self confirmMoveToTrash: NULL returnCode: NSAlertDefaultReturn contextInfo:[storiesToDelete retain]]; } } - (IBAction) revealInFinder: (id) sender { if ([self selectedFilename] != nil) { NSString* dir = [[self selectedFilename] stringByDeletingLastPathComponent]; BOOL isDir; if ([[NSFileManager defaultManager] fileExistsAtPath: dir isDirectory: &isDir]) { if (isDir) { [[NSWorkspace sharedWorkspace] openFile: dir]; } } } } // = windowWillClose, etc = - (void) windowWillClose: (NSNotification*) notification { } - (BOOL) windowShouldClose: (NSNotification*) notification { return YES; } // = Loading iFiction data = - (NSArray*) mergeiFictionFromMetabase: (ZoomMetadata*) newData { // Show our window [[self window] makeKeyAndOrderFront: self]; if (newData == nil) { // Doh! NSBeginAlertSheet(@"Unable to load metadata", @"Cancel", nil, nil, [self window], nil, nil, nil,nil, @"Zoom encountered an error while trying to load an iFiction file."); return nil; } if ([[newData errors] count] > 0) { NSBeginAlertSheet(@"Unable to load metadata", @"Cancel", nil, nil, [self window], nil, nil, nil,nil, @"Zoom encountered an error (%@) while trying to load an iFiction file.", [[newData errors] objectAtIndex: 0]); return nil; } // Merge any new descriptions found there NSMutableArray* replacements = [NSMutableArray array]; ZoomStory* story; NSEnumerator* storyEnum = [[newData stories] objectEnumerator]; while (story = [storyEnum nextObject]) { // Find if the story already exists in our index ZoomStory* oldStory = nil; ZoomStoryID* ident; NSEnumerator* identEnum = [[story storyIDs] objectEnumerator]; while (ident = [identEnum nextObject]) { oldStory = [[NSApp delegate] findStory: ident]; if (oldStory != nil) break; } if (oldStory != nil) { // Add this story to the list of stories to query about replacing [replacements addObject: story]; } // Add this story to the userMetadata [[[NSApp delegate] userMetadata] copyStory: story]; } // Store and reflect any changes [[[NSApp delegate] userMetadata] writeToDefaultFile]; [self reloadTableData]; [self configureFromMainTableSelection]; // The return value is the set of things that would be replaced return replacements; } - (void) mergeiFictionFromFile: (NSString*) filename { // Read the file ZoomMetadata* newData = [[[ZoomMetadata alloc] initWithContentsOfFile: filename] autorelease]; if (newData == nil) return; // Perform the merge NSArray* replacements = [self mergeiFictionFromMetabase: newData]; // If there's anything to query about, ask! if ([replacements count] > 0) { NSBeginAlertSheet(@"Some story descriptions are already in the database", @"Use new", @"Keep new", nil, [self window], self, @selector(useReplacements:returnCode:contextInfo:), nil, [replacements retain], @"This metadata file contains descriptions for some story files that already exist in the database. Do you want to keep using the old descriptions or switch to the new ones?"); } } - (void) useReplacements: (NSWindow *)alert returnCode: (int)returnCode contextInfo: (void *)contextInfo { NSArray* replacements = contextInfo; [replacements autorelease]; if (returnCode != NSAlertDefaultReturn) return; ZoomStory* story; NSEnumerator* storyEnum = [replacements objectEnumerator]; while (story = [storyEnum nextObject]) { [[[NSApp delegate] userMetadata] copyStory: story]; } // Store and reflect any changes [[[NSApp delegate] userMetadata] writeToDefaultFile]; [self reloadTableData]; [self configureFromMainTableSelection]; } // = Saving iFiction data = - (IBAction) saveMetadata: (id) sender { NSSavePanel* panel = [NSSavePanel savePanel]; [panel setRequiredFileType: @"iFiction"]; NSString* directory = [[NSUserDefaults standardUserDefaults] objectForKey: @"ZoomiFictionSavePath"]; [panel beginSheetForDirectory: directory file: nil modalForWindow: [self window] modalDelegate: self didEndSelector: @selector(saveMetadataDidEnd:returnCode:contextInfo:) contextInfo: nil]; } - (void) saveMetadataDidEnd: (NSSavePanel *) panel returnCode: (int) returnCode contextInfo: (void*) contextInfo { if (returnCode != NSOKButton) return; // Generate the data to save ZoomMetadata* newMetadata = [[[ZoomMetadata alloc] init] autorelease]; NSEnumerator* selEnum = [mainTableView selectedRowEnumerator]; NSNumber* selRow; while (selRow = [selEnum nextObject]) { ZoomStoryID* ident = [storyList objectAtIndex: [selRow intValue]]; ZoomStory* story = [[NSApp delegate] findStory: ident]; if (story != nil) { [newMetadata copyStory: story]; } } // Save it! [newMetadata writeToFile: [panel filename] atomically: YES]; // Store any preference changes [[NSUserDefaults standardUserDefaults] setObject: [panel directory] forKey: @"ZoomiFictionSavePath"]; } - (IBAction) flipToFilter: (id) sender { [self flipTo: filterView]; } - (IBAction) flipToInfo: (id) sender { [self flipTo: infoView]; } - (IBAction) flipToSaves: (id) sender { [self flipTo: saveGameView]; } // = ResourceDrop delegate = - (void) resourceDropFilenameChanged: (ZoomResourceDrop*) drop { ZoomStoryOrganiser* org = [ZoomStoryOrganiser sharedStoryOrganiser]; ZoomStory* selectedStory = [self selectedStory]; if (selectedStory != nil) { [selectedStory setObject: [drop droppedFilename] forKey: @"ResourceFilename"]; if ([[ZoomPreferences globalPreferences] keepGamesOrganised]) { [org organiseStory: selectedStory]; } } } // = Handling downloads = - (BOOL) canPlayFile: (NSString*) filename { NSArray* fileTypes = [NSArray arrayWithObjects: @"z3", @"z4", @"z5", @"z6", @"z7", @"z8", @"blorb", @"zblorb", @"blb", @"zlb", @"signpost", nil]; NSString* extn = [[filename pathExtension] lowercaseString]; if ([fileTypes containsObject: extn]) { return YES; } else if ([[ZoomPlugInManager sharedPlugInManager] plugInForFile: filename]) { return YES; } if ([extn isEqualToString: @"xml"] && [[[[filename stringByDeletingPathExtension] pathExtension] lowercaseString] isEqualToString: @"signpost"]) { return YES; } return NO; } - (void) addFilesFromDirectory: (NSString*) directory groupName: (NSString*) groupName { // Work out the group to use to store the files that we've added if (groupName == nil || [groupName length] == 0) groupName = @"Downloaded"; // Iterate through the directory and organise any files that we find NSMutableArray* addedFiles = [NSMutableArray array]; NSDirectoryEnumerator* dirEnum = [[NSFileManager defaultManager] enumeratorAtPath: directory]; NSString* path; NSString* signpostFile = nil; while (path = [dirEnum nextObject]) { path = [directory stringByAppendingPathComponent: path]; path = [path stringByStandardizingPath]; // Must exist BOOL isDir; if (![[NSFileManager defaultManager] fileExistsAtPath: path isDirectory: &isDir]) { continue; } // Must be a file if (isDir) continue; // Must be playable if (![self canPlayFile: path]) continue; // Could be a signpost if ([[[path pathExtension] lowercaseString] isEqualToString: @"signpost"]) { signpostFile = path; continue; } else if ([[[path pathExtension] lowercaseString] isEqualToString: @"xml"] && [[[[path stringByDeletingPathExtension] pathExtension] lowercaseString] isEqualToString: @"signpost"]) { signpostFile = path; continue; } // Must have an ID ZoomStoryID* storyId = [ZoomStoryID idForFile: path]; if (!storyId) continue; // Organise this file [[ZoomStoryOrganiser sharedStoryOrganiser] addStory: path withIdent: storyId organise: YES]; [addedFiles addObject: storyId]; } // Either catalogue the files as a group, or load the game that was downloaded if there's only one if ([addedFiles count] == 0) { if (signpostFile != nil) { // Play a signpost file [self openSignPost: [NSData dataWithContentsOfFile: signpostFile] forceDownload: YES]; return; } // Oops: couldn't find any games to add NSBeginAlertSheet(@"The download did not contain any story files", @"Cancel", nil, nil, [self window], nil, nil, nil, nil, @"Zoom successfully downloaded the file, but was unable to find any story files that can be played by the currently installed plugins."); } else if ([addedFiles count] == 1) { // Play one story ZoomStoryID* storyToPlay = [addedFiles lastObject]; NSString* filename = [[ZoomStoryOrganiser sharedStoryOrganiser] filenameForIdent: storyToPlay]; if (filename) { [[NSApp delegate] application: NSApp openFile: filename]; } // Select the story in the main table [mainTableView deselectAll: self]; int storyRow = [storyList indexOfObject: storyToPlay]; if (storyRow != NSNotFound) { [mainTableView selectRow: storyRow byExtendingSelection: NO]; [mainTableView scrollRowToVisible: storyRow]; } // Switch back to the browser view if (browserOn) [self showLocalGames: self]; } else { // Set the group of all the stories NSEnumerator* storyEnum = [addedFiles objectEnumerator]; ZoomStoryID* storyId; while (storyId = [storyEnum nextObject]) { ZoomStory* story = [self storyForID: storyId]; story = [self createStoryCopy: story]; [story setGroup: groupName]; /* [[ZoomStoryOrganiser sharedStoryOrganiser] addStory: [[ZoomStoryOrganiser sharedStoryOrganiser] filenameForIdent: storyId] withIdent: storyId organise: YES]; */ } // Set the filters to filter by group [mainTableView deselectAll: self]; [filterTable1 selectRow: 0 byExtendingSelection: NO]; [filterTable2 selectRow: 0 byExtendingSelection: NO]; NSTableColumn* filterColumn = [[filterTable1 tableColumns] objectAtIndex: 0]; [filterColumn setIdentifier: @"group"]; [self setTitle: @"Group" forTable: filterTable1]; [self reloadTableData]; [mainTableView reloadData]; // Filter by the group we just added int filterRow = [filterSet1 indexOfObject: groupName]; if (filterRow != NSNotFound) { [filterTable1 selectRow: filterRow+1 byExtendingSelection: NO]; } // If there's a signpost ID and we have that file, then open it if (signpostId) { NSString* storyFilename = [[ZoomStoryOrganiser sharedStoryOrganiser] filenameForIdent: signpostId]; if (storyFilename) { [[NSApp delegate] application: NSApp openFile: storyFilename]; } } // Switch back to the browser view if (browserOn) [self showLocalGames: self]; } // Write any new metadata [[[NSApp delegate] userMetadata] writeToDefaultFile]; [signpostId release]; signpostId = nil; } - (void) cancelFadeTimer { if (!downloadFadeTimer) return; [downloadFadeTimer autorelease]; [downloadFadeTimer invalidate]; downloadFadeTimer = nil; } - (void) finishPopDownload { [[[NSApp delegate] leopard] clearLayersForView: downloadView]; [[downloadView progress] startAnimation: self]; } - (void) finishPopOutDownload { [self cancelFadeTimer]; [[self window] removeChildWindow: downloadWindow]; [downloadWindow orderOut: self]; } - (void) showDownloadWindow { if ([downloadWindow isVisible]) return; // Display the window [[self window] addChildWindow: downloadWindow ordered: NSWindowAbove]; [downloadWindow orderFront: self]; [self positionDownloadWindow]; // Start the timer to fade the window in [self cancelFadeTimer]; if ([[NSApp delegate] leopard]) { // Fanicify the animation under leopard NSInvocation* finished = [NSInvocation invocationWithMethodSignature: [self methodSignatureForSelector: @selector(finishPopDownload)]]; [finished setTarget: self]; [finished setSelector: @selector(finishPopDownload)]; [[[NSApp delegate] leopard] popView: downloadView duration: 0.5 finished: finished]; [downloadWindow setAlphaValue: 1.0]; } else { // Use a more prosaic animation on Tiger initialDownloadOpacity = [downloadWindow alphaValue]; [downloadFadeStart release]; downloadFadeStart = [[NSDate date] retain]; downloadFadeTimer = [[NSTimer timerWithTimeInterval: 0.02 target: self selector: @selector(fadeDownloadIn) userInfo: nil repeats: YES] retain]; [[NSRunLoop currentRunLoop] addTimer: downloadFadeTimer forMode: NSDefaultRunLoopMode]; } } - (void) hideDownloadWindow: (double) duration { if (![downloadWindow isVisible]) return; // Start the timer to fade the window out [self cancelFadeTimer]; if ([[NSApp delegate] leopard]) { // Fanicify the animation under leopard [[downloadView progress] stopAnimation: self]; [[downloadView progress] setDoubleValue: 0]; NSInvocation* finished = [NSInvocation invocationWithMethodSignature: [self methodSignatureForSelector: @selector(finishPopOutDownload)]]; [finished setTarget: self]; [finished setSelector: @selector(finishPopOutDownload)]; [[[NSApp delegate] leopard] popOutView: downloadView duration: duration finished: finished]; [downloadWindow setAlphaValue: 1.0]; } else { // Tiger animation initialDownloadOpacity = [downloadWindow alphaValue]; [downloadFadeStart release]; downloadFadeStart = [[NSDate date] retain]; downloadFadeTimer = [[NSTimer timerWithTimeInterval: 0.02 target: self selector: @selector(fadeDownloadOut) userInfo: nil repeats: YES] retain]; [[NSRunLoop currentRunLoop] addTimer: downloadFadeTimer forMode: NSDefaultRunLoopMode]; } } - (void) fadeDownloadIn { NSTimeInterval runTime = [[NSDate date] timeIntervalSinceDate: downloadFadeStart]; double done = runTime / 0.5; if (done < 0) done = 0; if (done > 1) done = 1; done = -2.0*done*done*done + 3.0*done*done; [downloadWindow setAlphaValue: done]; if (done >= 0.999) { [self cancelFadeTimer]; } } - (void) fadeDownloadOut { NSTimeInterval runTime = [[NSDate date] timeIntervalSinceDate: downloadFadeStart]; double done = runTime / 0.5; if (done < 0) done = 0; if (done > 1) done = 1; done = -2.0*done*done*done + 3.0*done*done; [downloadWindow setAlphaValue: 1-done]; if (done >= 0.999) { [self cancelFadeTimer]; [[self window] removeChildWindow: downloadWindow]; [downloadWindow orderOut: self]; } } - (void) downloadStarting: (ZoomDownload*) download { if (download != activeDownload) return; [self showDownloadWindow]; [[downloadView progress] setIndeterminate: YES]; [[downloadView progress] setMinValue: 0]; [[downloadView progress] setMaxValue: 100.0]; if (![[NSApp delegate] leopard]) [[downloadView progress] startAnimation: self]; } - (void) downloadComplete: (ZoomDownload*) download { if (download != activeDownload) return; if (downloadUpdateList) { // We've downloaded a list of plugins, and we want to pick one to install as part of a signpost file downloadUpdateList = NO; NSString* xmlFile; NSString* extension = [[activeDownload suggestedFilename] pathExtension]; NSEnumerator* dirEnum = [[[NSFileManager defaultManager] directoryContentsAtPath: [activeDownload downloadDirectory]] objectEnumerator]; NSString* path; while (path = [dirEnum nextObject]) { if ([[path pathExtension] isEqualToString: extension]) { xmlFile = [[activeDownload downloadDirectory] stringByAppendingPathComponent: path]; } } if (xmlFile) { [self installSignpostPluginFrom: [NSData dataWithContentsOfFile: xmlFile]]; } else { // Butterfingers NSBeginAlertSheet(@"Could not download the plug-in", @"Cancel", nil, nil, [self window], nil, nil, nil, nil, @"Zoom succesfully downloaded a plugin update file, but was unable to locate it after the download had completed."); } } else if (downloadPlugin) { // We've downloaded a plugin and need to install it [self installPluginFrom: activeDownload]; } else { // Default: add story files [self addFilesFromDirectory: [download downloadDirectory] groupName: [[[download suggestedFilename] lastPathComponent] stringByDeletingPathExtension]]; } if (download == activeDownload) { [activeDownload setDelegate: nil]; [activeDownload autorelease]; activeDownload = nil; [[downloadView progress] stopAnimation: self]; [self hideDownloadWindow: 0.5]; } } - (void) downloadFailed: (ZoomDownload*) download reason: (NSString*) reason { if (download != activeDownload) return; [activeDownload setDelegate: nil]; [activeDownload autorelease]; activeDownload = nil; [[downloadView progress] stopAnimation: self]; NSBeginAlertSheet(@"Could not complete the download.", @"Cancel", nil, nil, [self window], nil, nil, nil, nil, [NSString stringWithFormat: @"An error was encountered while trying to download the requested file%@%@.", reason?@".\n\n":@"", reason]); [self hideDownloadWindow: 0.25]; } - (void) downloadUnarchiving: (ZoomDownload*) download { if (download != activeDownload) return; [[downloadView progress] setIndeterminate: YES]; } - (void) download: (ZoomDownload*) download completed: (float) complete { if (download != activeDownload) return; [[downloadView progress] setIndeterminate: NO]; [[downloadView progress] setDoubleValue: complete * 100.0]; } - (void) webView: (WebView *)sender decidePolicyForNavigationAction: (NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id)listener { NSArray* archiveFiles = [NSArray arrayWithObjects: @"zip", @"tar", @"tgz", @"gz", @"bz2", @"z", nil]; if ([[actionInformation objectForKey: WebActionNavigationTypeKey] intValue] == 0) { #ifdef DEVELOPMENT_BUILD NSLog(@"Deciding policy for %@", [request URL]); NSLog(@"User-agent is %@", [ifdbView userAgentForURL: [request URL]]); NSLog(@"(Custom agent is %@)", [ifdbView customUserAgent]); NSLog(@"Header fields are %@", [request allHTTPHeaderFields]); #endif // Get the URL to download NSURL* url = [request URL]; NSString* fakeFile = [[NSTemporaryDirectory() stringByAppendingPathComponent: @"FakeDirectory"] stringByAppendingPathComponent: [[url path] lastPathComponent]]; if ([self canPlayFile: fakeFile] || [archiveFiles containsObject: [[fakeFile pathExtension] lowercaseString]]) { // Use mirror.ifarchive.org, not www.ifarchive.org NSString* host = [url host]; if ([host isEqualToString: @"www.ifarchive.org"]) { url = [[[NSURL alloc] initWithScheme: [url scheme] host: @"mirror.ifarchive.org" path: [url path]] autorelease]; } // Download the specified file [activeDownload setDelegate: nil]; [activeDownload autorelease]; activeDownload = nil; [signpostId autorelease]; signpostId = nil; downloadUpdateList = NO; downloadPlugin = NO; activeDownload = [[ZoomDownload alloc] initWithUrl: url]; [activeDownload setDelegate: self]; [activeDownload startDownload]; // Ignore the request [listener ignore]; return; } } // Default is to use the request if ([NSURLConnection canHandleRequest: request]) { [listener use]; } else { [listener ignore]; } } - (void) webView:(WebView *)sender decidePolicyForMIMEType:(NSString *)type request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id)listener { if (![WebView canShowMIMEType: type]) { [listener ignore]; NSBeginAlertSheet(@"Zoom cannot download this type of file", @"Cancel", nil, nil, [self window], nil, nil, nil,nil, [NSString stringWithFormat: @"You have clicked on a download link that goes to a type of file that Zoom does not know how to handle. This could be because the file is a compressed file in a format that Zoom does not understand, or it could be because you have not installed the plug-in for this file type.\n\nYou can check for new plugins by using the 'Check for Updates' option in the Zoom menu.", type]); } else { [listener use]; } } // = Browsing the IFDB = - (void) updateBackForwardButtons { if ([ifdbView canGoForward]) { [forwardButton setEnabled: YES]; } else { [forwardButton setEnabled: NO]; } if ([ifdbView canGoBack]) { [backButton setEnabled: YES]; } else { [backButton setEnabled: NO]; } } - (NSString*) queryEncode: (NSString*) string { NSString* result = (NSString*)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)string, NULL, (CFStringRef)@"&?/=", kCFStringEncodingUTF8); return [result autorelease]; } - (IBAction) showIfDb: (id) sender { ZoomFlipView* fv = [[[ZoomFlipView alloc] init] autorelease]; [fv setAnimationTime: 0.35]; NSRect viewFrame = [mainView frame]; [fv setFrame: viewFrame]; [browserView setFrame: viewFrame]; [self setBrowserFontSize]; [fv prepareToAnimateView: mainView]; [fv animateTo: browserView style: ZoomAnimateRight]; NSString* ifdbUrl = [[[NSBundle mainBundle] infoDictionary] objectForKey: @"ZoomIfdbUrl"]; if (!ifdbUrl) { ifdbUrl = @"http://ifdb.tads.org/"; } // If the user has a single game selected, then open it in the browser BOOL findMore = NO; if ([mainTableView numberOfSelectedRows] == 1) { ZoomStoryID* ident = [storyList objectAtIndex: [mainTableView selectedRow]]; if (ident) { NSString* identString = [ident description]; ZoomStory* story = [self storyForID: ident]; if ([identString hasPrefix: @"UUID://"]) { identString = [identString substringFromIndex: 7]; identString = [identString substringToIndex: [identString length]-2]; } ifdbUrl = [NSString stringWithFormat: @"%@viewgame?ifid=%@&findmore", ifdbUrl, [self queryEncode: identString]]; if (story != nil) { ifdbUrl = [ifdbUrl stringByAppendingFormat: @"&title=%@", [self queryEncode: [story title]]]; } findMore = YES; } } // Reload the main page if the user has strayed off the main ifdb site NSURL* ifdb = [NSURL URLWithString: ifdbUrl]; if (findMore || !usedBrowser || [[[[[[ifdbView mainFrame] dataSource] request] URL] host] caseInsensitiveCompare: [ifdb host]] != 0) { if (!usedBrowser) { // Hacky way of clearing the history [[ifdbView backForwardList] setCapacity: 0]; [[ifdbView backForwardList] setCapacity: 256]; } if (![[[[[ifdbView mainFrame] dataSource] request] URL] isEqualTo: ifdb]) { [[ifdbView mainFrame] loadRequest: [NSURLRequest requestWithURL: ifdb]]; } } if (!browserOn && [[NSApp delegate] leopard]) { [[[NSApp delegate] leopard] popOutView: continueButton duration: 0.25 finished: nil]; [[[NSApp delegate] leopard] popOutView: newgameButton duration: 0.20 finished: nil]; [[[NSApp delegate] leopard] popOutView: addButton duration: 0.15 finished: nil]; [[[NSApp delegate] leopard] popOutView: infoButton duration: 0.20 finished: nil]; } [searchField setStringValue: @""]; usedBrowser = YES; browserOn = YES; [self updateButtons]; } - (IBAction) showLocalGames: (id) sender { ZoomFlipView* fv = [[[ZoomFlipView alloc] init] autorelease]; [fv setAnimationTime: 0.35]; NSRect viewFrame = [browserView frame]; [fv setFrame: viewFrame]; [mainView setFrame: viewFrame]; [searchField setStringValue: @""]; [fv prepareToAnimateView: browserView]; [fv animateTo: mainView style: ZoomAnimateLeft]; browserOn = NO; [self updateButtons]; if (!browserOn && [[NSApp delegate] leopard]) { [[[NSApp delegate] leopard] popView: continueButton duration: 0.15 finished: nil]; [[[NSApp delegate] leopard] popView: newgameButton duration: 0.25 finished: nil]; [[[NSApp delegate] leopard] popView: addButton duration: 0.35 finished: nil]; [[[NSApp delegate] leopard] popView: infoButton duration: 0.25 finished: nil]; } } - (void)webView:(WebView *)sender didStartProvisionalLoadForFrame:(WebFrame *)frame { [self updateBackForwardButtons]; if (frame == [ifdbView mainFrame]) { NSString* url = [[[[frame dataSource] request] URL] absoluteString]; if (url) [currentUrl setStringValue: url]; [progressIndicator startAnimation: self]; } } - (void)webView:(WebView *)sender didCommitLoadForFrame:(WebFrame *)frame { [self updateBackForwardButtons]; if (frame == [ifdbView mainFrame]) { NSString* url = [[[[frame dataSource] request] URL] absoluteString]; if (url) [currentUrl setStringValue: url]; [progressIndicator startAnimation: self]; // Cancel any running download if (activeDownload != nil && ![[[[frame dataSource] request] URL] isFileURL]) { [activeDownload setDelegate: nil]; [activeDownload autorelease]; activeDownload = nil; [self hideDownloadWindow: 0.25]; } } } - (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame { [self updateBackForwardButtons]; if (frame == [ifdbView mainFrame]) { NSString* url = [[[[frame dataSource] request] URL] absoluteString]; if (url) [currentUrl setStringValue: url]; [progressIndicator stopAnimation: self]; } } - (void)webView:(WebView *)sender didFailLoadWithError:(NSError *)error forFrame:(WebFrame *)frame { [self updateBackForwardButtons]; if (frame == [ifdbView mainFrame]) { NSString* url = [[[[frame dataSource] request] URL] absoluteString]; if (url) [currentUrl setStringValue: url]; [progressIndicator stopAnimation: self]; if (lastError == nil) lastError = [[ZoomJSError alloc] init]; [lastError setLastError: [error localizedDescription]]; switch ([error code]) { case 102: // Ignore these errors break; default: // Open the error page [[ifdbView mainFrame] loadRequest: [NSURLRequest requestWithURL: [NSURL fileURLWithPath: [[NSBundle mainBundle] pathForResource: @"ifdb-failed" ofType: @"html"]]]]; break; } } } - (void)webView:(WebView *)sender didFailProvisionalLoadWithError:(NSError *)error forFrame:(WebFrame *)frame { [self updateBackForwardButtons]; if (frame == [ifdbView mainFrame]) { NSString* url = [[[[frame dataSource] request] URL] absoluteString]; if (url) [currentUrl setStringValue: url]; [progressIndicator stopAnimation: self]; if (lastError == nil) lastError = [[ZoomJSError alloc] init]; [lastError setLastError: [error localizedDescription]]; switch ([error code]) { case 102: // Ignore these errors break; default: // Open the error page [[ifdbView mainFrame] loadRequest: [NSURLRequest requestWithURL: [NSURL fileURLWithPath: [[NSBundle mainBundle] pathForResource: @"ifdb-failed" ofType: @"html"]]]]; break; } } } - (void) webView:(WebView *)sender windowScriptObjectAvailable:(WebScriptObject *)windowScriptObject { // Attach the error object if (lastError == nil) { lastError = [[ZoomJSError alloc] init]; } [[sender windowScriptObject] setValue: lastError forKey: @"Errors"]; } - (IBAction) goBack: (id) sender { [ifdbView goBack]; } - (IBAction) goForward: (id) sender { [ifdbView goForward]; } - (IBAction) goHome: (id) sender; { NSString* ifdbUrl = [[[NSBundle mainBundle] infoDictionary] objectForKey: @"ZoomIfdbUrl"]; if (!ifdbUrl) { ifdbUrl = @"http://ifdb.tads.org/"; } [[ifdbView mainFrame] loadRequest: [NSURLRequest requestWithURL: [NSURL URLWithString: ifdbUrl]]]; } - (IBAction) playIfdbGame: (id) sender { } // = Dealing with signposts = - (void) openSignPost: (NSData*) signpostFile forceDownload: (BOOL) forceDownload { // Ensure that the window is displayed [self showWindow: self]; // Parse the property list ZoomSignPost* signpost = [[[ZoomSignPost alloc] initWithData: signpostFile] autorelease]; if (signpost == nil) { // Not a valid signpost return; } if ([signpost errorMessage]) { // Signpost is OK but just contains an error message NSBeginAlertSheet(@"IFDB has reported a problem with this game", @"Cancel", nil, nil, [self window], nil, nil, nil, nil, [NSString stringWithFormat: @"%@", [signpost errorMessage]]); return; } // Check for an installed plugin with the interpreter ID NSString* interpreter = [signpost interpreterDisplayName]; if (interpreter && [interpreter isKindOfClass: [NSString class]]) { BOOL haveInterpreter = NO; NSEnumerator* pluginEnum = [[[ZoomPlugInManager sharedPlugInManager] pluginBundles] objectEnumerator]; NSBundle* pluginBundle; while (pluginBundle = [pluginEnum nextObject]) { NSString* pluginName = [[ZoomPlugInManager sharedPlugInManager] nameForBundle: [pluginBundle bundlePath]]; if ([pluginName isEqualToString: interpreter]) { haveInterpreter = YES; break; } } if ([interpreter isEqualToString: @"Z-Code"]) { haveInterpreter = YES; } if (!haveInterpreter) { // Download the update file, then the interpreter NSString* updateUrl = [[signpost interpreterURL] absoluteString]; if (!updateUrl || ![updateUrl isKindOfClass: [NSString class]]) { return; } [activeDownload setDelegate: nil]; [activeDownload autorelease]; activeDownload = nil; [signpostId autorelease]; signpostId = nil; [activeSignpost release]; activeSignpost = [signpost retain]; NSBeginAlertSheet(@"Zoom needs to download a new plug-in in order play this story", @"Install plugin", @"Cancel", nil, [self window], self, @selector(downloadPlugIn:returnCode:contextInfo:), nil, nil, @"In order to play the story file you have selected, Zoom needs to download and install a new plug-in. If you choose to install this plug-in, Zoom will need to restart before the story can be played.\n\n" "Plug-ins contain interpreter programs necessary to run certain interactive fiction. Zoom comes with support for Z-Code, HUGO, TADS and Glulx formats but is capable of playing new formats by adding new plug-ins."); return; } } // See if the game is already present ZoomStoryID* downloadId = nil; NSEnumerator* idEnum = [[signpost ifids] objectEnumerator]; while (downloadId = [idEnum nextObject]) { // If we're not forcing a download, open the existing file if (downloadId && !forceDownload) { NSString* storyFilename = [[ZoomStoryOrganiser sharedStoryOrganiser] filenameForIdent: downloadId]; if (storyFilename) { [[NSApp delegate] application: NSApp openFile: storyFilename]; return; } } } if ([[signpost ifids] count] > 0) downloadId = [[signpost ifids] objectAtIndex: 0]; // Download the game NSURL* url = [signpost downloadURL]; if (url) { [activeDownload setDelegate: nil]; [activeDownload autorelease]; activeDownload = nil; [signpostId autorelease]; signpostId = [downloadId retain]; downloadUpdateList = NO; downloadPlugin = NO; // Use mirror.ifarchive.org, not www.ifarchive.org NSString* host = [url host]; if ([host isEqualToString: @"www.ifarchive.org"]) { url = [[[NSURL alloc] initWithScheme: [url scheme] host: @"mirror.ifarchive.org" path: [url path]] autorelease]; } activeDownload = [[ZoomDownload alloc] initWithUrl: url]; [activeDownload setDelegate: self]; [activeDownload startDownload]; } } - (void) downloadPlugIn: (NSWindow *)alert returnCode: (int)returnCode contextInfo: (void *)contextInfo { if (activeSignpost && returnCode == NSAlertDefaultReturn) { // Get the update URL NSURL* updateUrl = [activeSignpost interpreterURL]; if (!updateUrl) { return; } // Use mirror.ifarchive.org, not www.ifarchive.org NSString* host = [updateUrl host]; if ([host isEqualToString: @"www.ifarchive.org"]) { updateUrl = [[[NSURL alloc] initWithScheme: [updateUrl scheme] host: @"mirror.ifarchive.org" path: [updateUrl path]] autorelease]; } // Download the update XML file [activeDownload setDelegate: nil]; [activeDownload autorelease]; activeDownload = nil; [signpostId autorelease]; signpostId = nil; downloadUpdateList = YES; downloadPlugin = NO; activeDownload = [[ZoomDownload alloc] initWithUrl: updateUrl]; [activeDownload setDelegate: self]; [activeDownload startDownload]; } } - (void) failedToInstallPlugin: (NSString*) reason { NSBeginAlertSheet(@"Could not install the plug-in", @"Cancel", nil, nil, [self window], nil, nil, nil, nil, reason); } static unsigned int ValueForHexChar(int hex) { if (hex >= '0' && hex <= '9') return hex - '0'; if (hex >= 'a' && hex <= 'f') return hex - 'a' + 10; if (hex >= 'A' && hex <= 'F') return hex - 'A' + 10; return 0; } - (void) installSignpostPluginFrom: (NSData*) signpostXml { // Parse the property list NSDictionary* update = [NSPropertyListSerialization propertyListFromData: signpostXml mutabilityOption: NSPropertyListImmutable format: nil errorDescription: nil]; if (update == nil || ![update isKindOfClass: [NSDictionary class]]) { // Not a valid update file [self failedToInstallPlugin: @"The plugin update file that was download does not appear to contain a valid property list."]; return; } // Find a plugin that matches the signpost NSString* interpreter = [activeSignpost interpreterDisplayName]; if (!interpreter || ![interpreter isKindOfClass: [NSString class]]) { [self failedToInstallPlugin: @"Oops, Zoom forgot which interpreter it was trying to install."]; return; } NSEnumerator* pluginEnum = [[update allValues] objectEnumerator]; NSDictionary* plugInDetails; NSDictionary* pluginToInstall = nil; while (plugInDetails = [pluginEnum nextObject]) { if (![plugInDetails isKindOfClass: [NSDictionary class]]) { continue; } NSString* displayName = [plugInDetails objectForKey: @"DisplayName"]; if (!displayName || ![displayName isKindOfClass: [NSString class]]) { continue; } if ([displayName isEqualToString: interpreter]) { pluginToInstall = plugInDetails; break; } } if (!pluginToInstall) { [self failedToInstallPlugin: [NSString stringWithFormat: @"Zoom could not find a plugin for %@ at the specified site.", interpreter]]; return; } // Get the URL and MD5 for the plugin NSString* urlString = [pluginToInstall objectForKey: @"URL"]; NSString* md5raw = [pluginToInstall objectForKey: @"MD5"]; if (!urlString || !md5raw || ![urlString isKindOfClass: [NSString class]] || ![md5raw isKindOfClass: [NSString class]]) { [self failedToInstallPlugin: [NSString stringWithFormat: @"Zoom found a plugin for %@ at the specified site, but could not establish a URL to download it from.", interpreter]]; return; } // Build a digest from string values unsigned char digest[16]; int x; for (x=0; x<16; x++) { int pos = x*2; if (pos+1 >= [md5raw length]) break; unichar firstChar = [md5raw characterAtIndex: pos]; unichar secondChar = [md5raw characterAtIndex: pos+1]; digest[x] = (ValueForHexChar(firstChar)<<4)|ValueForHexChar(secondChar); } NSData* md5 = [[[NSData alloc] initWithBytes: digest length: 16] autorelease]; // Download the plugin [activeDownload setDelegate: nil]; [activeDownload autorelease]; activeDownload = nil; [signpostId autorelease]; signpostId = nil; downloadUpdateList = NO; downloadPlugin = YES; activeDownload = [[ZoomDownload alloc] initWithUrl: [NSURL URLWithString: urlString]]; [activeDownload setDelegate: self]; [activeDownload setExpectedMD5: md5]; [activeDownload startDownload]; } - (void) installPluginFrom: (ZoomDownload*) downloadedPlugin { NSEnumerator* downloadDirEnum = [[[NSFileManager defaultManager] directoryContentsAtPath: [downloadedPlugin downloadDirectory]] objectEnumerator]; NSString* path; BOOL installed = NO; while (!installed && (path = [downloadDirEnum nextObject])) { NSString* extension = [[path pathExtension] lowercaseString]; // Only install plugins if (![extension isEqualToString: @"plugin"] && ![extension isEqualToString: @"zoomplugin"]) { continue; } // We only install one plugin installed = YES; if (![[ZoomPlugInManager sharedPlugInManager] installPlugIn: [[downloadedPlugin downloadDirectory] stringByAppendingPathComponent: path]]) { [self failedToInstallPlugin: @"The plugin was successfully downloaded, but did not install correctly. This usually occurs because you do not have permission to modify the Zoom application."]; return; } } if (!installed) { [self failedToInstallPlugin: @"No plugins were found in the file that was downloaded."]; } // Restart if necessary if ([[ZoomPlugInManager sharedPlugInManager] restartRequired]) { // Write out a startup signpost file NSString* startupSignpost = [[[NSApp delegate] zoomConfigDirectory] stringByAppendingPathComponent: @"launch.signpost"]; NSData* signpostData = [activeSignpost data]; [signpostData writeToFile: startupSignpost atomically: YES]; // Restart Zoom [[ZoomPlugInController sharedPlugInController] restartZoom]; } else { // Retry the signpost [self openSignPost: [activeSignpost data] forceDownload: YES]; } } @end