1// 2// ZoomGlkWindowController.m 3// ZoomCocoa 4// 5// Created by Andrew Hunter on 24/11/2005. 6// Copyright 2005 Andrew Hunter. All rights reserved. 7// 8 9#import "ZoomGlkWindowController.h" 10#import "ZoomPreferences.h" 11#import "ZoomTextToSpeech.h" 12#import "ZoomSkeinController.h" 13#import "ZoomSkein.h" 14#import "ZoomGlkDocument.h" 15#import "ZoomGameInfoController.h" 16#import "ZoomNotesController.h" 17#import "ZoomWindowThatCanBecomeKey.h" 18#import "ZoomGlkSaveRef.h" 19#import "ZoomAppDelegate.h" 20#import "ZoomClearView.h" 21 22#import <GlkView/GlkHub.h> 23#import <GlkView/GlkView.h> 24#import <GlkView/GlkSessionProtocol.h> 25#import <GlkView/GlkFileRef.h> 26 27/// 28/// Class to interface to Zoom's skein system 29/// 30@interface ZoomGlkSkeinOutputReceiver : NSObject<GlkAutomation>{ 31 ZoomSkein* skein; 32} 33 34- (id) initWithSkein: (ZoomSkein*) skein; 35 36@end 37 38@implementation ZoomGlkSkeinOutputReceiver 39 40- (id) initWithSkein: (ZoomSkein*) newSkein { 41 self = [super init]; 42 43 if (self) { 44 skein = [newSkein retain]; 45 } 46 47 return self; 48} 49 50- (void) dealloc { 51 [skein release]; 52 [super dealloc]; 53} 54 55- (IBAction) glkTaskHasStarted: (id) sender { 56 [skein zoomInterpreterRestart]; 57} 58 59- (void) setGlkInputSource: (id) newSource { 60} 61 62- (void) receivedCharacters: (NSString*) characters 63 window: (int) windowNumber 64 fromView: (GlkView*) view { 65 [skein outputText: characters]; 66} 67 68- (void) userTyped: (NSString*) userInput 69 window: (int) windowNumber 70 lineInput: (BOOL) isLineInput 71 fromView: (GlkView*) view { 72 [skein zoomWaitingForInput]; 73 if (isLineInput) { 74 [skein inputCommand: userInput]; 75 } else { 76 [skein inputCharacter: userInput]; 77 } 78} 79 80- (void) userClickedAtXPos: (int) xpos 81 ypos: (int) ypos 82 window: (int) windowNumber 83 fromView: (GlkView*) view { 84} 85 86- (void) viewWaiting: (GlkView*) view { 87 // Do nothing 88} 89 90- (void) viewIsWaitingForInput: (GlkView*) view { 91} 92 93@end 94 95/// 96/// The window controller proper 97/// 98@interface ZoomGlkWindowController(ZoomPrivate) 99 100- (void) prefsChanged: (NSNotification*) not; 101 102@end 103 104@implementation ZoomGlkWindowController 105 106+ (void) initialize { 107 // Set up the Glk hub 108 [[GlkHub sharedGlkHub] useProcessHubName]; 109 [[GlkHub sharedGlkHub] setRandomHubCookie]; 110} 111 112// = Preferences = 113 114+ (GlkPreferences*) glkPreferencesFromZoomPreferences { 115 GlkPreferences* prefs = [[GlkPreferences alloc] init]; 116 ZoomPreferences* zPrefs = [ZoomPreferences globalPreferences]; 117 118 // Set the fonts according to the Zoom preferences object 119 [prefs setProportionalFont: [[zPrefs fonts] objectAtIndex: 0]]; 120 [prefs setFixedFont: [[zPrefs fonts] objectAtIndex: 4]]; 121 122 // Set the typography options according to the Zoom preferences object 123 [prefs setTextMargin: [zPrefs textMargin]]; 124 [prefs setUseScreenFonts: [zPrefs useScreenFonts]]; 125 [prefs setUseHyphenation: [zPrefs useHyphenation]]; 126 [prefs setUseKerning: [zPrefs useKerning]]; 127 [prefs setUseLigatures: [zPrefs useLigatures]]; 128 129 [prefs setScrollbackLength: [zPrefs scrollbackLength]]; 130 131 // Set the foreground/background colours 132 NSColor* foreground = [[zPrefs colours] objectAtIndex: [zPrefs foregroundColour]]; 133 NSColor* background = [[zPrefs colours] objectAtIndex: [zPrefs backgroundColour]]; 134 135 NSEnumerator* styleEnum = [[prefs styles] keyEnumerator]; 136 NSMutableDictionary* newStyles = [NSMutableDictionary dictionary]; 137 NSNumber* styleNum; 138 139 while (styleNum = [styleEnum nextObject]) { 140 GlkStyle* thisStyle = [[prefs styles] objectForKey: styleNum]; 141 142 [thisStyle setTextColour: foreground]; 143 [thisStyle setBackColour: background]; 144 145 [newStyles setObject: thisStyle 146 forKey: styleNum]; 147 } 148 149 [prefs setStyles: newStyles]; 150 151 return [prefs autorelease]; 152} 153 154// = Initialisation = 155 156- (id) init { 157 self = [super initWithWindowNibPath: [[NSBundle bundleForClass: [ZoomGlkWindowController class]] pathForResource: @"GlkWindow" 158 ofType: @"nib"] 159 owner: self]; 160 161 if (self) { 162 [[NSNotificationCenter defaultCenter] addObserver: self 163 selector: @selector(prefsChanged:) 164 name: ZoomPreferencesHaveChangedNotification 165 object: nil]; 166 167 skein = [[ZoomSkein alloc] init]; 168 } 169 170 return self; 171} 172 173- (void) dealloc { 174 [[NSNotificationCenter defaultCenter] removeObserver: self]; 175 176 [clientPath release]; 177 [inputPath release]; 178 [savedGamePath release]; 179 [logo release]; 180 [tts release]; 181 [skein release]; 182 [normalWindow release]; 183 [fullscreenWindow release]; 184 185 if (glkView) [glkView setDelegate: nil]; 186 187 [super dealloc]; 188} 189 190- (void) maybeStartView { 191 // If we're sufficiently configured to start the application, then do so 192 if (glkView && clientPath && inputPath) { 193 [tts release]; 194 tts = [[ZoomTextToSpeech alloc] init]; 195 [tts setSkein: skein]; 196 197 [glkView setDelegate: self]; 198 [glkView addOutputReceiver: [[[ZoomGlkSkeinOutputReceiver alloc] initWithSkein: skein] autorelease]]; 199 [glkView setPreferences: [ZoomGlkWindowController glkPreferencesFromZoomPreferences]]; 200 [glkView setInputFilename: inputPath]; 201 202 if (savedGamePath) { 203 if (canOpenSaveGames) { 204 NSString* saveSkeinPath = [savedGamePath stringByAppendingPathComponent: @"Skein.skein"]; 205 NSString* saveDataPath = [savedGamePath stringByAppendingPathComponent: @"Save.data"]; 206 207 if ([[NSFileManager defaultManager] fileExistsAtPath: saveDataPath]) { 208 [glkView addInputFilename: saveDataPath 209 withKey: @"savegame"]; 210 211 if ([[NSFileManager defaultManager] fileExistsAtPath: saveSkeinPath]) { 212 [skein parseXmlData: [NSData dataWithContentsOfFile: saveSkeinPath]]; 213 } 214 } 215 } 216 } 217 218 [glkView launchClientApplication: clientPath 219 withArguments: [NSArray array]]; 220 221 [self prefsChanged: nil]; 222 } 223} 224 225- (IBAction)showWindow:(id)sender { 226 [super showWindow: sender]; 227 228 if (savedGamePath && !canOpenSaveGames && !shownSaveGameWarning) { 229 shownSaveGameWarning = YES; 230 NSBeginAlertSheet(@"This interpreter is unable to load saved states", 231 @"Continue", nil, nil, 232 [self window], nil, nil, nil, nil, 233 @"Due to a limitation in the design of the interpreter for this story, Zoom is unable to request that it load a saved state file.\n\nYou will need to use the story's own restore function to request that it load the state that you selected."); 234 } 235} 236 237- (void) windowDidLoad { 238 // Configure the view 239 [glkView setRandomViewCookie]; 240 [logDrawer setLeadingOffset: 16]; 241 [logDrawer setContentSize: NSMakeSize([logDrawer contentSize].width, 120)]; 242 [logDrawer setMinContentSize: NSMakeSize(0, 120)]; 243 244 // Set the default log message 245 [logText setString: [NSString stringWithFormat: @"Zoom CocoaGlk Plugin\n"]]; 246 247 // Set up the window borders 248 if (![[ZoomPreferences globalPreferences] showGlkBorders]) 249 [glkView setBorderWidth: 0]; 250 else 251 [glkView setBorderWidth: 2]; 252 253 // Start it if we've got enough information 254 [self maybeStartView]; 255} 256 257- (void) prefsChanged: (NSNotification*) not { 258 // TODO: actually change the preferences (might need some changes to the way Glk styles work here; styles are traditionally fixed after they are set...) 259 if (glkView == nil) return; 260 261 if (!ttsAdded) [glkView addOutputReceiver: tts]; 262 ttsAdded = YES; 263 [tts setImmediate: [[ZoomPreferences globalPreferences] speakGameText]]; 264 265 // Set up the window borders 266 if (![[ZoomPreferences globalPreferences] showGlkBorders]) 267 [glkView setBorderWidth: 0]; 268 else 269 [glkView setBorderWidth: 2]; 270} 271 272// = Configuring the client = 273 274- (void) setClientPath: (NSString*) newPath { 275 // Set the client path 276 [clientPath release]; 277 clientPath = nil; 278 clientPath = [newPath copy]; 279 280 // Start it if we've got enough information 281 [self maybeStartView]; 282} 283 284- (void) setSaveGame: (NSString*) path { 285 // Set the saved game path 286 [savedGamePath release]; 287 savedGamePath = [path copy]; 288} 289 290- (void) setCanOpenSaveGame: (BOOL) newCanOpenSaveGame { 291 canOpenSaveGames = newCanOpenSaveGame; 292} 293 294- (void) setInputFilename: (NSString*) newPath { 295 // Set the input path 296 [inputPath release]; 297 inputPath = nil; 298 inputPath = [newPath copy]; 299 300 // Start it if we've got enough information 301 [self maybeStartView]; 302} 303 304- (void) setLogo: (NSImage*) newLogo { 305 [logo release]; 306 logo = [newLogo copy]; 307} 308 309- (BOOL) disableLogo { 310 return logo == nil || ![[ZoomPreferences globalPreferences] showCoverPicture]; 311} 312 313- (NSImage*) logo { 314 return logo; 315} 316 317- (NSString*) preferredSaveDirectory { 318 if (!canOpenSaveGames && savedGamePath) { 319 // If the user has requested a particular save game and the interpreter doesn't know how to load it, then open the directory containing the game that they wanted 320 return [savedGamePath stringByDeletingLastPathComponent]; 321 } else { 322 // Otherwise use whatever the document thinks should be used 323 return [[self document] preferredSaveDirectory]; 324 } 325} 326 327// = Log messages = 328 329- (void) showLogMessage: (NSString*) message 330 withStatus: (GlkLogStatus) status { 331 // Choose a style for this message 332 float msgSize = 10; 333 NSColor* msgColour = [NSColor grayColor]; 334 BOOL isBold = NO; 335 336 switch (status) { 337 case GlkLogRoutine: 338 break; 339 340 case GlkLogInformation: 341 isBold = YES; 342 break; 343 344 case GlkLogCustom: 345 msgSize = 12; 346 msgColour = [NSColor blackColor]; 347 break; 348 349 case GlkLogWarning: 350 msgColour = [NSColor blueColor]; 351 msgSize = 12; 352 break; 353 354 case GlkLogError: 355 msgSize = 12; 356 msgColour = [NSColor redColor]; 357 isBold = YES; 358 break; 359 360 case GlkLogFatalError: 361 msgSize = 12; 362 msgColour = [NSColor colorWithDeviceRed: 0.8 363 green: 0 364 blue: 0 365 alpha: 1.0]; 366 isBold = YES; 367 break; 368 } 369 370 // Create the attributes for this style 371 NSFont* font; 372 373 if (isBold) { 374 font = [NSFont boldSystemFontOfSize: msgSize]; 375 } else { 376 font = [NSFont systemFontOfSize: msgSize]; 377 } 378 379 NSDictionary* msgAttributes = [NSDictionary dictionaryWithObjectsAndKeys: 380 font, NSFontAttributeName, 381 msgColour, NSForegroundColorAttributeName, 382 nil]; 383 384 // Create the attributed string 385 NSAttributedString* newMsg = [[NSAttributedString alloc] initWithString: [message stringByAppendingString: @"\n"] 386 attributes: msgAttributes]; 387 388 // Append this message to the log 389 [[logText textStorage] appendAttributedString: [newMsg autorelease]]; 390 391 // Show the log drawer 392 if (status >= GlkLogWarning && (status >= GlkLogFatalError || [[ZoomPreferences globalPreferences] displayWarnings])) { 393 [logDrawer open: self]; 394 } 395} 396 397- (void) showLog: (id) sender { 398 [logDrawer open: self]; 399} 400 401- (void) windowWillClose: (NSNotification*) not { 402 [glkView terminateClient]; 403} 404 405// = The game info window = 406 407- (IBAction) recordGameInfo: (id) sender { 408 ZoomGameInfoController* sgI = [ZoomGameInfoController sharedGameInfoController]; 409 ZoomStory* storyInfo = [(ZoomGlkDocument*)[self document] storyData]; 410 411 if ([sgI gameInfo] == storyInfo) { 412 NSDictionary* sgIValues = [sgI dictionary]; 413 414 [storyInfo setTitle: [sgIValues objectForKey: @"title"]]; 415 [storyInfo setHeadline: [sgIValues objectForKey: @"headline"]]; 416 [storyInfo setAuthor: [sgIValues objectForKey: @"author"]]; 417 [storyInfo setGenre: [sgIValues objectForKey: @"genre"]]; 418 [storyInfo setYear: [[sgIValues objectForKey: @"year"] intValue]]; 419 [storyInfo setGroup: [sgIValues objectForKey: @"group"]]; 420 [storyInfo setComment: [sgIValues objectForKey: @"comments"]]; 421 [storyInfo setTeaser: [sgIValues objectForKey: @"teaser"]]; 422 [storyInfo setZarfian: [[sgIValues objectForKey: @"zarfRating"] unsignedIntValue]]; 423 [storyInfo setRating: [[sgIValues objectForKey: @"rating"] floatValue]]; 424 425 [[(id)[NSApp delegate] userMetadata] writeToDefaultFile]; 426 } 427} 428 429- (IBAction) updateGameInfo: (id) sender { 430 if ([[ZoomGameInfoController sharedGameInfoController] infoOwner] == self) { 431 [[ZoomGameInfoController sharedGameInfoController] setGameInfo: [(ZoomGlkDocument*)[self document] storyData]]; 432 } 433} 434 435// = Gaining/losing focus = 436 437- (void)windowDidBecomeMain:(NSNotification *)aNotification { 438 [[ZoomSkeinController sharedSkeinController] setSkein: skein]; 439 440 [[ZoomGameInfoController sharedGameInfoController] setInfoOwner: self]; 441 [[ZoomGameInfoController sharedGameInfoController] setGameInfo: [(ZoomGlkDocument*)[self document] storyData]]; 442 443 [[ZoomNotesController sharedNotesController] setGameInfo: [(ZoomGlkDocument*)[self document] storyData]]; 444 [[ZoomNotesController sharedNotesController] setInfoOwner: self]; 445} 446 447- (void)windowDidResignMain:(NSNotification *)aNotification { 448 if ([[ZoomGameInfoController sharedGameInfoController] infoOwner] == self) { 449 [self recordGameInfo: self]; 450 451 [[ZoomGameInfoController sharedGameInfoController] setGameInfo: nil]; 452 [[ZoomGameInfoController sharedGameInfoController] setInfoOwner: nil]; 453 } 454 455 if ([[ZoomNotesController sharedNotesController] infoOwner] == self) { 456 [[ZoomNotesController sharedNotesController] setGameInfo: nil]; 457 [[ZoomNotesController sharedNotesController] setInfoOwner: nil]; 458 } 459 460 if ([[ZoomSkeinController sharedSkeinController] skein] == skein) { 461 [[ZoomSkeinController sharedSkeinController] setSkein: nil]; 462 } 463} 464 465// = Closing the window = 466 467- (void) confirmFinish:(NSWindow *)sheet 468 returnCode:(int)returnCode 469 contextInfo:(void *)contextInfo { 470 if (returnCode == NSAlertDefaultReturn) { 471 // Close the window 472 closeConfirmed = YES; 473 [[NSRunLoop currentRunLoop] performSelector: @selector(performClose:) 474 target: [self window] 475 argument: self 476 order: 32 477 modes: [NSArray arrayWithObject: NSDefaultRunLoopMode]]; 478 } 479} 480 481- (BOOL) windowShouldClose: (id) sender { 482 // Get confirmation if required 483 if (!closeConfirmed && running && [[ZoomPreferences globalPreferences] confirmGameClose]) { 484 BOOL autosave = [[ZoomPreferences globalPreferences] autosaveGames]; 485 NSString* msg; 486 487 msg = @"There is still a story playing in this window. Are you sure you wish to finish it without saving? The current state of the game will be lost."; 488 489 NSBeginAlertSheet(@"Finish the game?", 490 @"Finish", @"Continue playing", nil, 491 [self window], self, 492 @selector(confirmFinish:returnCode:contextInfo:), nil, 493 nil, msg); 494 495 return NO; 496 } 497 498 return YES; 499} 500 501// = Going fullscreen = 502 503- (IBAction) playInFullScreen: (id) sender { 504 if (isFullscreen) { 505 // Show the menubar 506 [NSMenu setMenuBarVisible: YES]; 507 508 // Stop being fullscreen 509 [glkView retain]; 510 [glkView removeFromSuperview]; 511 512 [glkView setScaleFactor: 1.0]; 513 [glkView setFrame: [[normalWindow contentView] bounds]]; 514 [[normalWindow contentView] addSubview: glkView]; 515 [glkView release]; 516 517 // Swap windows back 518 if (normalWindow) { 519 [fullscreenWindow setDelegate: nil]; 520 [fullscreenWindow setInitialFirstResponder: nil]; 521 522 [normalWindow setDelegate: self]; 523 [normalWindow setWindowController: self]; 524 [self setWindow: normalWindow]; 525 [normalWindow setInitialFirstResponder: glkView]; 526 [normalWindow setFrame: oldWindowFrame 527 display: YES]; 528 [normalWindow makeKeyAndOrderFront: self]; 529 530 [fullscreenWindow orderOut: self]; 531 [fullscreenWindow release]; fullscreenWindow = nil; 532 } 533 534 //[self setWindowFrameAutosaveName: @"ZoomClientWindow"]; 535 isFullscreen = NO; 536 } else { 537 // Do nothing if the game is not running 538 if (!running) return; 539 540 // As of 10.4, we need to create a separate full-screen window (10.4 tries to be 'clever' with the window borders, which messes things up 541 if (!normalWindow) normalWindow = [[self window] retain]; 542 if (!fullscreenWindow) { 543 fullscreenWindow = [[ZoomWindowThatCanBecomeKey alloc] initWithContentRect: [[[self window] contentView] bounds] 544 styleMask: NSBorderlessWindowMask 545 backing: NSBackingStoreBuffered 546 defer: YES]; 547 548 [fullscreenWindow setLevel: NSFloatingWindowLevel]; 549 [fullscreenWindow setHidesOnDeactivate: YES]; 550 [fullscreenWindow setReleasedWhenClosed: NO]; 551 [fullscreenWindow setOpaque: NO]; 552 if ([[NSApp delegate] leopard]) { 553 [fullscreenWindow setBackgroundColor: [NSColor clearColor]]; 554 } 555 556 if (![fullscreenWindow canBecomeKeyWindow]) { 557 [NSException raise: @"ZoomProgrammerIsASpoon" 558 format: @"For some reason, the full screen window won't accept key"]; 559 } 560 } 561 562 // Swap the displayed windows over 563 [self setWindowFrameAutosaveName: @""]; 564 [fullscreenWindow setFrame: [normalWindow frame] 565 display: NO]; 566 [fullscreenWindow makeKeyAndOrderFront: self]; 567 568 [glkView retain]; 569 [glkView removeFromSuperview]; 570 [[fullscreenWindow contentView] addSubview: glkView]; 571 [glkView release]; 572 573 [normalWindow setInitialFirstResponder: nil]; 574 [normalWindow setDelegate: nil]; 575 576 [fullscreenWindow setInitialFirstResponder: glkView]; 577 [fullscreenWindow makeFirstResponder: glkView]; 578 [fullscreenWindow setDelegate: self]; 579 580 [fullscreenWindow setWindowController: self]; 581 [self setWindow: fullscreenWindow]; 582 583 // Start being fullscreen 584 [[self window] makeKeyAndOrderFront: self]; 585 oldWindowFrame = [[self window] frame]; 586 587 // Finish off glkView 588 NSSize oldGlkViewSize = [glkView frame].size; 589 590 [glkView retain]; 591 [glkView removeFromSuperviewWithoutNeedingDisplay]; 592 593 // Hide the menubar 594 [NSMenu setMenuBarVisible: NO]; 595 596 // Resize the window 597 NSRect frame = [[[self window] screen] frame]; 598 if (![[NSApp delegate] leopard]) { 599 [[self window] setShowsResizeIndicator: NO]; 600 frame = [NSWindow frameRectForContentRect: frame 601 styleMask: NSBorderlessWindowMask]; 602 [[self window] setFrame: frame 603 display: YES 604 animate: YES]; 605 [normalWindow orderOut: self]; 606 } else { 607 [[self window] setContentView: [[[ZoomClearView alloc] init] autorelease]]; 608 [[self window] setFrame: frame 609 display: YES 610 animate: NO]; 611 } 612 613 // Resize, reposition the glkView 614 NSRect newGlkViewFrame = [[[self window] contentView] bounds]; 615 NSRect newGlkViewBounds; 616 617 newGlkViewBounds.origin = NSMakePoint(0,0); 618 newGlkViewBounds.size = newGlkViewFrame.size; 619 620 double ratio = newGlkViewFrame.size.width/oldGlkViewSize.width; 621 [glkView setFrame: newGlkViewFrame]; 622 [glkView setScaleFactor: ratio]; 623 624 // Add it back in again 625 [[[self window] contentView] addSubview: glkView]; 626 [glkView release]; 627 628 // Perform an animation in Leopard 629 if ([[NSApp delegate] leopard]) { 630 [[[NSApp delegate] leopard] fullScreenView: glkView 631 fromFrame: oldWindowFrame 632 toFrame: frame]; 633 } 634 635 isFullscreen = YES; 636 } 637} 638 639// = Ending the game = 640 641- (void) taskHasStarted { 642 [[self window] setDocumentEdited: YES]; 643 644 running = YES; 645 closeConfirmed = NO; 646} 647 648- (void) taskHasCrashed { 649 [[self window] setTitle: [NSString stringWithFormat: @"%@ (crashed)", [[self document] displayName], nil]]; 650} 651 652- (void) taskHasFinished { 653 if (isFullscreen) [self playInFullScreen: self]; 654 655 [[self window] setTitle: [NSString stringWithFormat: @"%@ (finished)", [[self document] displayName], nil]]; 656 657 [[self window] setDocumentEdited: NO]; 658 running = NO; 659} 660 661// = Saving the game = 662 663- (BOOL) promptForFilesForUsage: (NSString*) usage 664 forWriting: (BOOL) writing 665 handler: (NSObject<GlkFilePrompt>*) handler 666 preferredDirectory: (NSString*) preferredDirectory { 667 if (![usage isEqualToString: GlkFileUsageSavedGame]) { 668 // We only customise save game generation 669 return NO; 670 } 671 672 // Remember the handler 673 [promptHandler release]; 674 promptHandler = [handler retain]; 675 676 // Create the prompt window 677 if (writing) { 678 // Create a save dialog 679 NSSavePanel* panel = [NSSavePanel savePanel]; 680 681 [panel setRequiredFileType: @"glksave"]; 682 if (preferredDirectory != nil) [panel setDirectory: preferredDirectory]; 683 684 [panel beginSheetForDirectory: preferredDirectory 685 file: nil 686 modalForWindow: [self window] 687 modalDelegate: self 688 didEndSelector: @selector(panelDidEnd:returnCode:contextInfo:) 689 contextInfo: nil]; 690 691 [lastPanel release]; lastPanel = [panel retain]; 692 } else { 693 // Create an open dialog 694 NSOpenPanel* panel = [NSOpenPanel openPanel]; 695 696 NSMutableArray* allowedFiletypes = [[[glkView fileTypesForUsage: usage] mutableCopy] autorelease]; 697 [allowedFiletypes insertObject: @"glksave" 698 atIndex: 0]; 699 700 [panel setRequiredFileType: [allowedFiletypes objectAtIndex: 0]]; 701 if (preferredDirectory != nil) [panel setDirectory: preferredDirectory]; 702 703 if ([panel respondsToSelector: @selector(setAllowedFileTypes:)]) { 704 // Only works on 10.3 705 [panel setAllowedFileTypes: allowedFiletypes]; 706 } 707 708 [panel beginSheetForDirectory: preferredDirectory 709 file: nil 710 types: allowedFiletypes 711 modalForWindow: [self window] 712 modalDelegate: self 713 didEndSelector: @selector(panelDidEnd:returnCode:contextInfo:) 714 contextInfo: nil]; 715 716 [lastPanel release]; lastPanel = [panel retain]; 717 } 718 719 return YES; 720} 721 722- (void) panelDidEnd: (NSSavePanel*) panel 723 returnCode: (int) returnCode 724 contextInfo: (void*) willBeNil { 725 if (!promptHandler) return; 726 727 if (returnCode == NSOKButton) { 728 // TODO: preview 729 if ([[[[panel filename] pathExtension] lowercaseString] isEqualToString: @"glksave"]) { 730 ZoomGlkSaveRef* saveRef = [[ZoomGlkSaveRef alloc] initWithPlugIn: [[self document] plugIn] 731 path: [panel filename]]; 732 [saveRef setSkein: skein]; 733 [promptHandler promptedFileRef: saveRef]; 734 [saveRef autorelease]; 735 } else { 736 GlkFileRef* promptRef = [[GlkFileRef alloc] initWithPath: [panel filename]]; 737 [promptHandler promptedFileRef: promptRef]; 738 [promptRef autorelease]; 739 } 740 741 [[NSUserDefaults standardUserDefaults] setObject: [panel directory] 742 forKey: @"GlkSaveDirectory"]; 743 if ([self respondsToSelector: @selector(savePreferredDirectory:)]) { 744 [self savePreferredDirectory: [panel directory]]; 745 } 746 } else { 747 [promptHandler promptCancelled]; 748 } 749 750 [promptHandler release]; promptHandler = nil; 751 [lastPanel release]; lastPanel = nil; 752} 753 754// = Speech commands = 755 756- (IBAction) stopSpeakingMove: (id) sender { 757 [tts beQuiet]; 758} 759 760- (IBAction) speakMostRecent: (id) sender { 761 [tts resetMoves]; 762 [tts speakLastText]; 763} 764 765- (IBAction) speakNext: (id) sender { 766 [tts speakNextMove]; 767} 768 769- (IBAction) speakPrevious: (id) sender { 770 [tts speakPreviousMove]; 771} 772 773@end 774