OSDN Git Service

When the JP preprocessor macro is set, detect and render EUC-JP encoded characters...
[hengbandforosx/hengbandosx.git] / src / main-cocoa.m
1 /**
2  * \file main-cocoa.m
3  * \brief OS X front end
4  *
5  * Copyright (c) 2011 Peter Ammon
6  *
7  * This work is free software; you can redistribute it and/or modify it
8  * under the terms of either:
9  *
10  * a) the GNU General Public License as published by the Free Software
11  *    Foundation, version 2, or
12  *
13  * b) the "Angband licence":
14  *    This software may be copied and distributed for educational, research,
15  *    and not for profit purposes provided that this copyright and statement
16  *    are included in all such copies.  Other copyrights may also apply.
17  */
18
19 #include "angband.h"
20 /* This is not included in angband.h in Hengband. */
21 #include "grafmode.h"
22
23 #if defined(MACH_O_COCOA)
24
25 /* Mac headers */
26 #include <Cocoa/Cocoa.h>
27 //#include <Carbon/Carbon.h> /* For keycodes */
28 /* Hack - keycodes to enable compiling in macOS 10.14 */
29 #define kVK_Return 0x24
30 #define kVK_Tab    0x30
31 #define kVK_Delete 0x33
32 #define kVK_Escape 0x35
33 #define kVK_ANSI_KeypadEnter 0x4C
34
35 static NSSize const AngbandScaleIdentity = {1.0, 1.0};
36 static NSString * const AngbandDirectoryNameLib = @"lib";
37 static NSString * const AngbandDirectoryNameBase = @"Hengband";
38
39 static NSString * const AngbandTerminalsDefaultsKey = @"Terminals";
40 static NSString * const AngbandTerminalRowsDefaultsKey = @"Rows";
41 static NSString * const AngbandTerminalColumnsDefaultsKey = @"Columns";
42 static NSString * const AngbandTerminalVisibleDefaultsKey = @"Visible";
43 static NSString * const AngbandGraphicsDefaultsKey = @"GraphicsID";
44 static NSString * const AngbandFrameRateDefaultsKey = @"FramesPerSecond";
45 static NSString * const AngbandSoundDefaultsKey = @"AllowSound";
46 static NSInteger const AngbandWindowMenuItemTagBase = 1000;
47 static NSInteger const AngbandCommandMenuItemTagBase = 2000;
48
49 /* We can blit to a large layer or image and then scale it down during live
50  * resize, which makes resizing much faster, at the cost of some image quality
51  * during resizing */
52 #ifndef USE_LIVE_RESIZE_CACHE
53 # define USE_LIVE_RESIZE_CACHE 1
54 #endif
55
56 /* Global defines etc from Angband 3.5-dev - NRM */
57 #define ANGBAND_TERM_MAX 8
58
59 static bool new_game = TRUE;
60
61 #define MAX_COLORS 256
62 #define MSG_MAX SOUND_MAX
63
64 /* End Angband stuff - NRM */
65
66 /* Application defined event numbers */
67 enum
68 {
69     AngbandEventWakeup = 1
70 };
71
72 /* Redeclare some 10.7 constants and methods so we can build on 10.6 */
73 enum
74 {
75     Angband_NSWindowCollectionBehaviorFullScreenPrimary = 1 << 7,
76     Angband_NSWindowCollectionBehaviorFullScreenAuxiliary = 1 << 8
77 };
78
79 @interface NSWindow (AngbandLionRedeclares)
80 - (void)setRestorable:(BOOL)flag;
81 @end
82
83 /* Delay handling of pre-emptive "quit" event */
84 static BOOL quit_when_ready = FALSE;
85
86 /* Set to indicate the game is over and we can quit without delay */
87 static Boolean game_is_finished = FALSE;
88
89 /* Our frames per second (e.g. 60). A value of 0 means unthrottled. */
90 static int frames_per_second;
91
92 /* Function to get the default font */
93 static NSFont *default_font;
94
95 @class AngbandView;
96
97 /* The max number of glyphs we support.  Currently this only affects
98  * updateGlyphInfo() for the calculation of the tile size (the glyphArray and
99  * glyphWidths members of AngbandContext are only used in updateGlyphInfo()).
100  * The rendering in drawWChar will work for glyphs not in updateGlyphInfo()'s
101  * set, and that is used for rendering Japanese characters.
102  */
103 #define GLYPH_COUNT 256
104
105 /* An AngbandContext represents a logical Term (i.e. what Angband thinks is
106  * a window). This typically maps to one NSView, but may map to more than one
107  * NSView (e.g. the Test and real screen saver view). */
108 @interface AngbandContext : NSObject <NSWindowDelegate>
109 {
110 @public
111     
112     /* The Angband term */
113     term *terminal;
114     
115     /* Column and row cont, by default 80 x 24 */
116     size_t cols;
117     size_t rows;
118     
119     /* The size of the border between the window edge and the contents */
120     NSSize borderSize;
121     
122     /* Our array of views */
123     NSMutableArray *angbandViews;
124     
125     /* The buffered image */
126     CGLayerRef angbandLayer;
127
128     /* The font of this context */
129     NSFont *angbandViewFont;
130     
131     /* If this context owns a window, here it is */
132     NSWindow *primaryWindow;
133     
134     /* "Glyph info": an array of the CGGlyphs and their widths corresponding to
135          * the above font. */
136     CGGlyph glyphArray[GLYPH_COUNT];
137     CGFloat glyphWidths[GLYPH_COUNT];
138     
139     /* The size of one tile */
140     NSSize tileSize;
141     
142     /* Font's descender */
143     CGFloat fontDescender;
144     
145     /* Whether we are currently in live resize, which affects how big we render
146          * our image */
147     int inLiveResize;
148     
149     /* Last time we drew, so we can throttle drawing */
150     CFAbsoluteTime lastRefreshTime;
151     
152 @private
153
154     BOOL _hasSubwindowFlags;
155     BOOL _windowVisibilityChecked;
156 }
157
158 @property (nonatomic, assign) BOOL hasSubwindowFlags;
159 @property (nonatomic, assign) BOOL windowVisibilityChecked;
160
161 - (void)drawRect:(NSRect)rect inView:(NSView *)view;
162
163 /* Called at initialization to set the term */
164 - (void)setTerm:(term *)t;
165
166 /* Called when the context is going down. */
167 - (void)dispose;
168
169 /* Returns the size of the image. */
170 - (NSSize)imageSize;
171
172 /* Return the rect for a tile at given coordinates. */
173 - (NSRect)rectInImageForTileAtX:(int)x Y:(int)y;
174
175 /* Draw the given wide character into the given tile rect. */
176 - (void)drawWChar:(wchar_t)wchar inRect:(NSRect)tile;
177
178 /* Locks focus on the Angband image, and scales the CTM appropriately. */
179 - (CGContextRef)lockFocus;
180
181 /* Locks focus on the Angband image but does NOT scale the CTM. Appropriate
182  * for drawing hairlines. */
183 - (CGContextRef)lockFocusUnscaled;
184
185 /* Unlocks focus. */
186 - (void)unlockFocus;
187
188 /* Returns the primary window for this angband context, creating it if
189  * necessary */
190 - (NSWindow *)makePrimaryWindow;
191
192 /* Called to add a new Angband view */
193 - (void)addAngbandView:(AngbandView *)view;
194
195 /* Make the context aware that one of its views changed size */
196 - (void)angbandViewDidScale:(AngbandView *)view;
197
198 /* Handle becoming the main window */
199 - (void)windowDidBecomeMain:(NSNotification *)notification;
200
201 /* Return whether the context's primary window is ordered in or not */
202 - (BOOL)isOrderedIn;
203
204 /* Return whether the context's primary window is key */
205 - (BOOL)isMainWindow;
206
207 /* Invalidate the whole image */
208 - (void)setNeedsDisplay:(BOOL)val;
209
210 /* Invalidate part of the image, with the rect expressed in base coordinates */
211 - (void)setNeedsDisplayInBaseRect:(NSRect)rect;
212
213 /* Display (flush) our Angband views */
214 - (void)displayIfNeeded;
215
216 /* Resize context to size of contentRect, and optionally save size to
217  * defaults */
218 - (void)resizeTerminalWithContentRect: (NSRect)contentRect saveToDefaults: (BOOL)saveToDefaults;
219
220 /* Called from the view to indicate that it is starting or ending live resize */
221 - (void)viewWillStartLiveResize:(AngbandView *)view;
222 - (void)viewDidEndLiveResize:(AngbandView *)view;
223 - (void)saveWindowVisibleToDefaults: (BOOL)windowVisible;
224 - (BOOL)windowVisibleUsingDefaults;
225
226 /* Class methods */
227
228 /* Begins an Angband game. This is the entry point for starting off. */
229 + (void)beginGame;
230
231 /* Ends an Angband game. */
232 + (void)endGame;
233
234 /* Internal method */
235 - (AngbandView *)activeView;
236
237 @end
238
239 /**
240  * Generate a mask for the subwindow flags. The mask is just a safety check to
241  * make sure that our windows show and hide as expected.  This function allows
242  * for future changes to the set of flags without needed to update it here
243  * (unless the underlying types change).
244  */
245 u32b AngbandMaskForValidSubwindowFlags(void)
246 {
247     int windowFlagBits = sizeof(*(window_flag)) * CHAR_BIT;
248     int maxBits = MIN( 16, windowFlagBits );
249     u32b mask = 0;
250
251     for( int i = 0; i < maxBits; i++ )
252     {
253         if( window_flag_desc[i] != NULL )
254         {
255             mask |= (1 << i);
256         }
257     }
258
259     return mask;
260 }
261
262 /**
263  * Check for changes in the subwindow flags and update window visibility.
264  * This seems to be called for every user event, so we don't
265  * want to do any unnecessary hiding or showing of windows.
266  */
267 static void AngbandUpdateWindowVisibility(void)
268 {
269     /* Because this function is called frequently, we'll make the mask static.
270          * It doesn't change between calls, as the flags themselves are hardcoded */
271     static u32b validWindowFlagsMask = 0;
272
273     if( validWindowFlagsMask == 0 )
274     {
275         validWindowFlagsMask = AngbandMaskForValidSubwindowFlags();
276     }
277
278     /* Loop through all of the subwindows and see if there is a change in the
279          * flags. If so, show or hide the corresponding window. We don't care about
280          * the flags themselves; we just want to know if any are set. */
281     for( int i = 1; i < ANGBAND_TERM_MAX; i++ )
282     {
283         AngbandContext *angbandContext = angband_term[i]->data;
284
285         if( angbandContext == nil )
286         {
287             continue;
288         }
289
290         /* This horrible mess of flags is so that we can try to maintain some
291                  * user visibility preference. This should allow the user a window and
292                  * have it stay closed between application launches. However, this
293                  * means that when a subwindow is turned on, it will no longer appear
294                  * automatically. Angband has no concept of user control over window
295                  * visibility, other than the subwindow flags. */
296         if( !angbandContext.windowVisibilityChecked )
297         {
298             if( [angbandContext windowVisibleUsingDefaults] )
299             {
300                 [angbandContext->primaryWindow orderFront: nil];
301                 angbandContext.windowVisibilityChecked = YES;
302             }
303             else
304             {
305                 [angbandContext->primaryWindow close];
306                 angbandContext.windowVisibilityChecked = NO;
307             }
308         }
309         else
310         {
311             BOOL termHasSubwindowFlags = ((window_flag[i] & validWindowFlagsMask) > 0);
312
313             if( angbandContext.hasSubwindowFlags && !termHasSubwindowFlags )
314             {
315                 [angbandContext->primaryWindow close];
316                 angbandContext.hasSubwindowFlags = NO;
317                 [angbandContext saveWindowVisibleToDefaults: NO];
318             }
319             else if( !angbandContext.hasSubwindowFlags && termHasSubwindowFlags )
320             {
321                 [angbandContext->primaryWindow orderFront: nil];
322                 angbandContext.hasSubwindowFlags = YES;
323                 [angbandContext saveWindowVisibleToDefaults: YES];
324             }
325         }
326     }
327
328     /* Make the main window key so that user events go to the right spot */
329     AngbandContext *mainWindow = angband_term[0]->data;
330     [mainWindow->primaryWindow makeKeyAndOrderFront: nil];
331 }
332
333 /**
334  * Here is some support for rounding to pixels in a scaled context
335  */
336 static double push_pixel(double pixel, double scale, BOOL increase)
337 {
338     double scaledPixel = pixel * scale;
339     /* Have some tolerance! */
340     double roundedPixel = round(scaledPixel);
341     if (fabs(roundedPixel - scaledPixel) <= .0001)
342     {
343         scaledPixel = roundedPixel;
344     }
345     else
346     {
347         scaledPixel = (increase ? ceil : floor)(scaledPixel);
348     }
349     return scaledPixel / scale;
350 }
351
352 /* Descriptions of how to "push pixels" in a given rect to integralize.
353  * For example, PUSH_LEFT means that we round expand the left edge if set,
354  * otherwise we shrink it. */
355 enum
356 {
357     PUSH_LEFT = 0x1,
358     PUSH_RIGHT = 0x2,
359     PUSH_BOTTOM = 0x4,
360     PUSH_TOP = 0x8
361 };
362
363 /**
364  * Return a rect whose border is in the "cracks" between tiles
365  */
366 static NSRect crack_rect(NSRect rect, NSSize scale, unsigned pushOptions)
367 {
368     double rightPixel = push_pixel(NSMaxX(rect), scale.width, !! (pushOptions & PUSH_RIGHT));
369     double topPixel = push_pixel(NSMaxY(rect), scale.height, !! (pushOptions & PUSH_TOP));
370     double leftPixel = push_pixel(NSMinX(rect), scale.width, ! (pushOptions & PUSH_LEFT));
371     double bottomPixel = push_pixel(NSMinY(rect), scale.height, ! (pushOptions & PUSH_BOTTOM));
372     return NSMakeRect(leftPixel, bottomPixel, rightPixel - leftPixel, topPixel - bottomPixel);    
373 }
374
375 /**
376  * Returns the pixel push options (describing how we round) for the tile at a
377  * given index. Currently it's pretty uniform!
378  */
379 static unsigned push_options(unsigned x, unsigned y)
380 {
381     return PUSH_TOP | PUSH_LEFT;
382 }
383
384 /**
385  * ------------------------------------------------------------------------
386  * Graphics support
387  * ------------------------------------------------------------------------ */
388
389 /**
390  * The tile image
391  */
392 static CGImageRef pict_image;
393
394 /**
395  * Numbers of rows and columns in a tileset,
396  * calculated by the PICT/PNG loading code
397  */
398 static int pict_cols = 0;
399 static int pict_rows = 0;
400
401 /**
402  * Requested graphics mode (as a grafID).
403  * The current mode is stored in current_graphics_mode.
404  */
405 static int graf_mode_req = 0;
406
407 /**
408  * Helper function to check the various ways that graphics can be enabled,
409  * guarding against NULL
410  */
411 static BOOL graphics_are_enabled(void)
412 {
413     return current_graphics_mode
414         && current_graphics_mode->grafID != GRAPHICS_NONE;
415 }
416
417 /**
418  * Hack -- game in progress
419  */
420 static Boolean game_in_progress = FALSE;
421
422
423 #pragma mark Prototypes
424 static void wakeup_event_loop(void);
425 static void hook_plog(const char *str);
426 static void hook_quit(const char * str);
427 static void load_prefs(void);
428 static void load_sounds(void);
429 static void init_windows(void);
430 static void handle_open_when_ready(void);
431 static void play_sound(int event);
432 static BOOL check_events(int wait);
433 static BOOL send_event(NSEvent *event);
434 static void record_current_savefile(void);
435 #ifdef JP
436 static wchar_t convert_two_byte_eucjp_to_utf16_native(const char *cp);
437 #endif
438
439 /**
440  * Available values for 'wait'
441  */
442 #define CHECK_EVENTS_DRAIN -1
443 #define CHECK_EVENTS_NO_WAIT    0
444 #define CHECK_EVENTS_WAIT 1
445
446
447 /**
448  * Note when "open"/"new" become valid
449  */
450 static bool initialized = FALSE;
451
452 /* Methods for getting the appropriate NSUserDefaults */
453 @interface NSUserDefaults (AngbandDefaults)
454 + (NSUserDefaults *)angbandDefaults;
455 @end
456
457 @implementation NSUserDefaults (AngbandDefaults)
458 + (NSUserDefaults *)angbandDefaults
459 {
460     return [NSUserDefaults standardUserDefaults];
461 }
462 @end
463
464 /* Methods for pulling images out of the Angband bundle (which may be separate
465  * from the current bundle in the case of a screensaver */
466 @interface NSImage (AngbandImages)
467 + (NSImage *)angbandImage:(NSString *)name;
468 @end
469
470 /* The NSView subclass that draws our Angband image */
471 @interface AngbandView : NSView
472 {
473     IBOutlet AngbandContext *angbandContext;
474 }
475
476 - (void)setAngbandContext:(AngbandContext *)context;
477 - (AngbandContext *)angbandContext;
478
479 @end
480
481 @implementation NSImage (AngbandImages)
482
483 /* Returns an image in the resource directoy of the bundle containing the
484  * Angband view class. */
485 + (NSImage *)angbandImage:(NSString *)name
486 {
487     NSBundle *bundle = [NSBundle bundleForClass:[AngbandView class]];
488     NSString *path = [bundle pathForImageResource:name];
489     NSImage *result;
490     if (path) result = [[[NSImage alloc] initByReferencingFile:path] autorelease];
491     else result = nil;
492     return result;
493 }
494
495 @end
496
497
498 @implementation AngbandContext
499
500 @synthesize hasSubwindowFlags=_hasSubwindowFlags;
501 @synthesize windowVisibilityChecked=_windowVisibilityChecked;
502
503 - (NSFont *)selectionFont
504 {
505     return angbandViewFont;
506 }
507
508 - (BOOL)useLiveResizeOptimization
509 {
510     /* If we have graphics turned off, text rendering is fast enough that we
511          * don't need to use a live resize optimization. */
512     return inLiveResize && graphics_are_enabled();
513 }
514
515 - (NSSize)baseSize
516 {
517     /* We round the base size down. If we round it up, I believe we may end up
518          * with pixels that nobody "owns" that may accumulate garbage. In general
519          * rounding down is harmless, because any lost pixels may be sopped up by
520          * the border. */
521     return NSMakeSize(floor(cols * tileSize.width + 2 * borderSize.width), floor(rows * tileSize.height + 2 * borderSize.height));
522 }
523
524 /* qsort-compatible compare function for CGSizes */
525 static int compare_advances(const void *ap, const void *bp)
526 {
527     const CGSize *a = ap, *b = bp;
528     return (a->width > b->width) - (a->width < b->width);
529 }
530
531 - (void)updateGlyphInfo
532 {
533     /* Update glyphArray and glyphWidths */
534     NSFont *screenFont = [angbandViewFont screenFont];
535
536     /* Generate a string containing each MacRoman character */
537     unsigned char latinString[GLYPH_COUNT];
538     size_t i;
539     for (i=0; i < GLYPH_COUNT; i++) latinString[i] = (unsigned char)i;
540     
541     /* Turn that into unichar. Angband uses ISO Latin 1. */
542     unichar unicharString[GLYPH_COUNT] = {0};
543     NSString *allCharsString = [[NSString alloc] initWithBytes:latinString length:sizeof latinString encoding:NSISOLatin1StringEncoding];
544     [allCharsString getCharacters:unicharString range:NSMakeRange(0, MIN(GLYPH_COUNT, [allCharsString length]))];
545     [allCharsString autorelease];
546     
547     /* Get glyphs */
548     memset(glyphArray, 0, sizeof glyphArray);
549     CTFontGetGlyphsForCharacters((CTFontRef)screenFont, unicharString, glyphArray, GLYPH_COUNT);
550     
551     /* Get advances. Record the max advance. */
552     CGSize advances[GLYPH_COUNT] = {};
553     CTFontGetAdvancesForGlyphs((CTFontRef)screenFont, kCTFontHorizontalOrientation, glyphArray, advances, GLYPH_COUNT);
554     for (i=0; i < GLYPH_COUNT; i++) {
555         glyphWidths[i] = advances[i].width;
556     }
557     
558     /* For good non-mono-font support, use the median advance. Start by sorting
559          * all advances. */
560     qsort(advances, GLYPH_COUNT, sizeof *advances, compare_advances);
561     
562     /* Skip over any initially empty run */
563     size_t startIdx;
564     for (startIdx = 0; startIdx < GLYPH_COUNT; startIdx++)
565     {
566         if (advances[startIdx].width > 0) break;
567     }
568     
569     /* Pick the center to find the median */
570     CGFloat medianAdvance = 0;
571     if (startIdx < GLYPH_COUNT)
572     {
573                 /* In case we have all zero advances for some reason */
574         medianAdvance = advances[(startIdx + GLYPH_COUNT)/2].width;
575     }
576     
577     /* Record the descender */
578     fontDescender = [screenFont descender];
579     
580     /* Record the tile size. Note that these are typically fractional values -
581          * which seems sketchy, but we end up scaling the heck out of our view
582          * anyways, so it seems to not matter. */
583     tileSize.width = medianAdvance;
584     tileSize.height = [screenFont ascender] - [screenFont descender];
585 }
586
587 - (void)updateImage
588 {
589     NSSize size = NSMakeSize(1, 1);
590     
591     AngbandView *activeView = [self activeView];
592     if (activeView)
593     {
594         /* If we are in live resize, draw as big as the screen, so we can scale
595                  * nicely to any size. If we are not in live resize, then use the
596                  * bounds of the active view. */
597         NSScreen *screen;
598         if ([self useLiveResizeOptimization] && (screen = [[activeView window] screen]) != NULL)
599         {
600             size = [screen frame].size;
601         }
602         else
603         {
604             size = [activeView bounds].size;
605         }
606     }
607
608     CGLayerRelease(angbandLayer);
609     
610     /* Use the highest monitor scale factor on the system to work out what
611      * scale to draw at - not the recommended method, but works where we
612      * can't easily get the monitor the current draw is occurring on. */
613     float angbandLayerScale = 1.0;
614     if ([[NSScreen mainScreen] respondsToSelector:@selector(backingScaleFactor)]) {
615         for (NSScreen *screen in [NSScreen screens]) {
616             angbandLayerScale = fmax(angbandLayerScale, [screen backingScaleFactor]);
617         }
618     }
619
620     /* Make a bitmap context as an example for our layer */
621     CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
622     CGContextRef exampleCtx = CGBitmapContextCreate(NULL, 1, 1, 8 /* bits per component */, 48 /* bytesPerRow */, cs, kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host);
623     CGColorSpaceRelease(cs);
624
625     /* Create the layer at the appropriate size */
626     size.width = fmax(1, ceil(size.width * angbandLayerScale));
627     size.height = fmax(1, ceil(size.height * angbandLayerScale));
628     angbandLayer = CGLayerCreateWithContext(exampleCtx, *(CGSize *)&size, NULL);
629
630     CFRelease(exampleCtx);
631
632     /* Set the new context of the layer to draw at the correct scale */
633     CGContextRef ctx = CGLayerGetContext(angbandLayer);
634     CGContextScaleCTM(ctx, angbandLayerScale, angbandLayerScale);
635
636     [self lockFocus];
637     [[NSColor blackColor] set];
638     NSRectFill((NSRect){NSZeroPoint, [self baseSize]});
639     [self unlockFocus];
640 }
641
642 - (void)requestRedraw
643 {
644     if (! self->terminal) return;
645     
646     term *old = Term;
647     
648     /* Activate the term */
649     Term_activate(self->terminal);
650     
651     /* Redraw the contents */
652     Term_redraw();
653     
654     /* Flush the output */
655     Term_fresh();
656     
657     /* Restore the old term */
658     Term_activate(old);
659 }
660
661 - (void)setTerm:(term *)t
662 {
663     terminal = t;
664 }
665
666 - (void)viewWillStartLiveResize:(AngbandView *)view
667 {
668 #if USE_LIVE_RESIZE_CACHE
669     if (inLiveResize < INT_MAX) inLiveResize++;
670     else [NSException raise:NSInternalInconsistencyException format:@"inLiveResize overflow"];
671     
672     if (inLiveResize == 1 && graphics_are_enabled())
673     {
674         [self updateImage];
675         
676         [self setNeedsDisplay:YES]; /* We'll need to redisplay everything anyways, so avoid creating all those little redisplay rects */
677         [self requestRedraw];
678     }
679 #endif
680 }
681
682 - (void)viewDidEndLiveResize:(AngbandView *)view
683 {
684 #if USE_LIVE_RESIZE_CACHE
685     if (inLiveResize > 0) inLiveResize--;
686     else [NSException raise:NSInternalInconsistencyException format:@"inLiveResize underflow"];
687     
688     if (inLiveResize == 0 && graphics_are_enabled())
689     {
690         [self updateImage];
691         
692         [self setNeedsDisplay:YES]; /* We'll need to redisplay everything anyways, so avoid creating all those little redisplay rects */
693         [self requestRedraw];
694     }
695 #endif
696 }
697
698 /**
699  * If we're trying to limit ourselves to a certain number of frames per second,
700  * then compute how long it's been since we last drew, and then wait until the
701  * next frame has passed. */
702 - (void)throttle
703 {
704     if (frames_per_second > 0)
705     {
706         CFAbsoluteTime now = CFAbsoluteTimeGetCurrent();
707         CFTimeInterval timeSinceLastRefresh = now - lastRefreshTime;
708         CFTimeInterval timeUntilNextRefresh = (1. / (double)frames_per_second) - timeSinceLastRefresh;
709         
710         if (timeUntilNextRefresh > 0)
711         {
712             usleep((unsigned long)(timeUntilNextRefresh * 1000000.));
713         }
714     }
715     lastRefreshTime = CFAbsoluteTimeGetCurrent();
716 }
717
718 - (void)drawWChar:(wchar_t)wchar inRect:(NSRect)tile
719 {
720     CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort];
721     CGFloat tileOffsetY = CTFontGetAscent( (CTFontRef)[angbandViewFont screenFont] );
722     CGFloat tileOffsetX = 0.0;
723     NSFont *screenFont = [angbandViewFont screenFont];
724     UniChar unicharString[2] = {(UniChar)wchar, 0};
725
726     /* Get glyph and advance */
727     CGGlyph thisGlyphArray[1] = { 0 };
728     CGSize advances[1] = { { 0, 0 } };
729     CTFontGetGlyphsForCharacters((CTFontRef)screenFont, unicharString, thisGlyphArray, 1);
730     CGGlyph glyph = thisGlyphArray[0];
731     CTFontGetAdvancesForGlyphs((CTFontRef)screenFont, kCTFontHorizontalOrientation, thisGlyphArray, advances, 1);
732     CGSize advance = advances[0];
733     
734     /* If our font is not monospaced, our tile width is deliberately not big
735          * enough for every character. In that event, if our glyph is too wide, we
736          * need to compress it horizontally. Compute the compression ratio.
737          * 1.0 means no compression. */
738     double compressionRatio;
739     if (advance.width <= NSWidth(tile))
740     {
741         /* Our glyph fits, so we can just draw it, possibly with an offset */
742         compressionRatio = 1.0;
743         tileOffsetX = (NSWidth(tile) - advance.width)/2;
744     }
745     else
746     {
747         /* Our glyph doesn't fit, so we'll have to compress it */
748         compressionRatio = NSWidth(tile) / advance.width;
749         tileOffsetX = 0;
750     }
751
752     
753     /* Now draw it */
754     CGAffineTransform textMatrix = CGContextGetTextMatrix(ctx);
755     CGFloat savedA = textMatrix.a;
756
757     /* Set the position */
758     textMatrix.tx = tile.origin.x + tileOffsetX;
759     textMatrix.ty = tile.origin.y + tileOffsetY;
760
761     /* Maybe squish it horizontally. */
762     if (compressionRatio != 1.)
763     {
764         textMatrix.a *= compressionRatio;
765     }
766
767     textMatrix = CGAffineTransformScale( textMatrix, 1.0, -1.0 );
768     CGContextSetTextMatrix(ctx, textMatrix);
769     CGContextShowGlyphsWithAdvances(ctx, &glyph, &CGSizeZero, 1);
770     
771     /* Restore the text matrix if we messed with the compression ratio */
772     if (compressionRatio != 1.)
773     {
774         textMatrix.a = savedA;
775         CGContextSetTextMatrix(ctx, textMatrix);
776     }
777
778     textMatrix = CGAffineTransformScale( textMatrix, 1.0, -1.0 );
779     CGContextSetTextMatrix(ctx, textMatrix);
780 }
781
782 /* Lock and unlock focus on our image or layer, setting up the CTM
783  * appropriately. */
784 - (CGContextRef)lockFocusUnscaled
785 {
786     /* Create an NSGraphicsContext representing this CGLayer */
787     CGContextRef ctx = CGLayerGetContext(angbandLayer);
788     NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithGraphicsPort:ctx flipped:NO];
789     [NSGraphicsContext saveGraphicsState];
790     [NSGraphicsContext setCurrentContext:context];
791     CGContextSaveGState(ctx);
792     return ctx;
793 }
794
795 - (void)unlockFocus
796 {
797     /* Restore the graphics state */
798     CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort];
799     CGContextRestoreGState(ctx);
800     [NSGraphicsContext restoreGraphicsState];
801 }
802
803 - (NSSize)imageSize
804 {
805     /* Return the size of our layer */
806     CGSize result = CGLayerGetSize(angbandLayer);
807     return NSMakeSize(result.width, result.height);
808 }
809
810 - (CGContextRef)lockFocus
811 {
812     return [self lockFocusUnscaled];
813 }
814
815
816 - (NSRect)rectInImageForTileAtX:(int)x Y:(int)y
817 {
818     int flippedY = y;
819     return NSMakeRect(x * tileSize.width + borderSize.width, flippedY * tileSize.height + borderSize.height, tileSize.width, tileSize.height);
820 }
821
822 - (void)setSelectionFont:(NSFont*)font adjustTerminal: (BOOL)adjustTerminal
823 {
824     /* Record the new font */
825     [font retain];
826     [angbandViewFont release];
827     angbandViewFont = font;
828     
829     /* Update our glyph info */
830     [self updateGlyphInfo];
831
832     if( adjustTerminal )
833     {
834         /* Adjust terminal to fit window with new font; save the new columns
835                  * and rows since they could be changed */
836         NSRect contentRect = [self->primaryWindow contentRectForFrameRect: [self->primaryWindow frame]];
837         [self resizeTerminalWithContentRect: contentRect saveToDefaults: YES];
838     }
839
840     /* Update our image */
841     [self updateImage];
842     
843     /* Get redrawn */
844     [self requestRedraw];
845 }
846
847 - (id)init
848 {
849     if ((self = [super init]))
850     {
851         /* Default rows and cols */
852         self->cols = 80;
853         self->rows = 24;
854
855         /* Default border size */
856         self->borderSize = NSMakeSize(2, 2);
857
858         /* Allocate our array of views */
859         angbandViews = [[NSMutableArray alloc] init];
860         
861         /* Make the image. Since we have no views, it'll just be a puny 1x1 image. */
862         [self updateImage];
863
864         _windowVisibilityChecked = NO;
865     }
866     return self;
867 }
868
869 /**
870  * Destroy all the receiver's stuff. This is intended to be callable more than
871  * once.
872  */
873 - (void)dispose
874 {
875     terminal = NULL;
876     
877     /* Disassociate ourselves from our angbandViews */
878     [angbandViews makeObjectsPerformSelector:@selector(setAngbandContext:) withObject:nil];
879     [angbandViews release];
880     angbandViews = nil;
881     
882     /* Destroy the layer/image */
883     CGLayerRelease(angbandLayer);
884     angbandLayer = NULL;
885
886     /* Font */
887     [angbandViewFont release];
888     angbandViewFont = nil;
889     
890     /* Window */
891     [primaryWindow setDelegate:nil];
892     [primaryWindow close];
893     [primaryWindow release];
894     primaryWindow = nil;
895 }
896
897 /* Usual Cocoa fare */
898 - (void)dealloc
899 {
900     [self dispose];
901     [super dealloc];
902 }
903
904
905
906 #pragma mark -
907 #pragma mark Directories and Paths Setup
908
909 /**
910  * Return the path for Angband's lib directory and bail if it isn't found. The
911  * lib directory should be in the bundle's resources directory, since it's
912  * copied when built.
913  */
914 + (NSString *)libDirectoryPath
915 {
916     NSString *bundleLibPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent: AngbandDirectoryNameLib];
917     BOOL isDirectory = NO;
918     BOOL libExists = [[NSFileManager defaultManager] fileExistsAtPath: bundleLibPath isDirectory: &isDirectory];
919
920     if( !libExists || !isDirectory )
921     {
922         NSLog( @"[%@ %@]: can't find %@/ in bundle: isDirectory: %d libExists: %d", NSStringFromClass( [self class] ), NSStringFromSelector( _cmd ), AngbandDirectoryNameLib, isDirectory, libExists );
923         NSRunAlertPanel( @"Missing Resources", @"Hengband was unable to find required resources and must quit. Please report a bug on the Angband forums.", @"Quit", nil, nil );
924         exit( 0 );
925     }
926
927         return bundleLibPath;
928 }
929
930 /**
931  * Return the path for the directory where Angband should look for its standard
932  * user file tree.
933  */
934 + (NSString *)angbandDocumentsPath
935 {
936         NSString *documents = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
937
938 #if defined(SAFE_DIRECTORY)
939         NSString *versionedDirectory = [NSString stringWithFormat: @"%@-%s", AngbandDirectoryNameBase, VERSION_STRING];
940         return [documents stringByAppendingPathComponent: versionedDirectory];
941 #else
942         return [documents stringByAppendingPathComponent: AngbandDirectoryNameBase];
943 #endif
944 }
945
946 /**
947  * Adjust directory paths as needed to correct for any differences needed by
948  * Angband. \c init_file_paths() currently requires that all paths provided have
949  * a trailing slash and all other platforms honor this.
950  *
951  * \param originalPath The directory path to adjust.
952  * \return A path suitable for Angband or nil if an error occurred.
953  */
954 static NSString *AngbandCorrectedDirectoryPath(NSString *originalPath)
955 {
956         if ([originalPath length] == 0) {
957                 return nil;
958         }
959
960         if (![originalPath hasSuffix: @"/"]) {
961                 return [originalPath stringByAppendingString: @"/"];
962         }
963
964         return originalPath;
965 }
966
967 /**
968  * Give Angband the base paths that should be used for the various directories
969  * it needs. It will create any needed directories.
970  */
971 + (void)prepareFilePathsAndDirectories
972 {
973         char libpath[PATH_MAX + 1] = "\0";
974         NSString *libDirectoryPath = AngbandCorrectedDirectoryPath([self libDirectoryPath]);
975         [libDirectoryPath getFileSystemRepresentation: libpath maxLength: sizeof(libpath)];
976
977         char basepath[PATH_MAX + 1] = "\0";
978         NSString *angbandDocumentsPath = AngbandCorrectedDirectoryPath([self angbandDocumentsPath]);
979         [angbandDocumentsPath getFileSystemRepresentation: basepath maxLength: sizeof(basepath)];
980
981         init_file_paths(libpath, libpath, basepath);
982         create_needed_dirs();
983 }
984
985 #pragma mark -
986
987 #if 0
988 /* From the Linux mbstowcs(3) man page:
989  *   If dest is NULL, n is ignored, and the conversion  proceeds  as  above,
990  *   except  that  the converted wide characters are not written out to mem‐
991  *   ory, and that no length limit exists.
992  */
993 static size_t Term_mbcs_cocoa(wchar_t *dest, const char *src, int n)
994 {
995     int i;
996     int count = 0;
997
998     /* Unicode code point to UTF-8
999      *  0x0000-0x007f:   0xxxxxxx
1000      *  0x0080-0x07ff:   110xxxxx 10xxxxxx
1001      *  0x0800-0xffff:   1110xxxx 10xxxxxx 10xxxxxx
1002      * 0x10000-0x1fffff: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
1003      * Note that UTF-16 limits Unicode to 0x10ffff. This code is not
1004      * endian-agnostic.
1005      */
1006     for (i = 0; i < n || dest == NULL; i++) {
1007         if ((src[i] & 0x80) == 0) {
1008             if (dest != NULL) dest[count] = src[i];
1009             if (src[i] == 0) break;
1010         } else if ((src[i] & 0xe0) == 0xc0) {
1011             if (dest != NULL) dest[count] = 
1012                             (((unsigned char)src[i] & 0x1f) << 6)| 
1013                             ((unsigned char)src[i+1] & 0x3f);
1014             i++;
1015         } else if ((src[i] & 0xf0) == 0xe0) {
1016             if (dest != NULL) dest[count] = 
1017                             (((unsigned char)src[i] & 0x0f) << 12) | 
1018                             (((unsigned char)src[i+1] & 0x3f) << 6) |
1019                             ((unsigned char)src[i+2] & 0x3f);
1020             i += 2;
1021         } else if ((src[i] & 0xf8) == 0xf0) {
1022             if (dest != NULL) dest[count] = 
1023                             (((unsigned char)src[i] & 0x0f) << 18) | 
1024                             (((unsigned char)src[i+1] & 0x3f) << 12) |
1025                             (((unsigned char)src[i+2] & 0x3f) << 6) |
1026                             ((unsigned char)src[i+3] & 0x3f);
1027             i += 3;
1028         } else {
1029             /* Found an invalid multibyte sequence */
1030             return (size_t)-1;
1031         }
1032         count++;
1033     }
1034     return count;
1035 }
1036 #endif
1037
1038 /**
1039  * Entry point for initializing Angband
1040  */
1041 + (void)beginGame
1042 {
1043     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1044     
1045     /* Hooks in some "z-util.c" hooks */
1046     plog_aux = hook_plog;
1047     quit_aux = hook_quit;
1048     
1049     /* Initialize file paths */
1050     [self prepareFilePathsAndDirectories];
1051
1052     /* Load preferences */
1053     load_prefs();
1054     
1055     /* Prepare the windows */
1056     init_windows();
1057     
1058     /* Set up game event handlers */
1059     /* init_display(); */
1060     
1061     /* Register the sound hook */
1062     /* sound_hook = play_sound; */
1063     
1064     /* Initialise game */
1065     init_angband();
1066
1067     /* This is not incorporated into Hengband's init_angband() yet. */
1068     init_graphics_modes();
1069
1070     /* Note the "system" */
1071     ANGBAND_SYS = "mac";
1072     
1073     /* Initialize some save file stuff */
1074     player_egid = getegid();
1075     
1076     /* We are now initialized */
1077     initialized = TRUE;
1078     
1079     /* Handle "open_when_ready" */
1080     handle_open_when_ready();
1081     
1082     /* Handle pending events (most notably update) and flush input */
1083     Term_flush();
1084
1085     /* Prompt the user. */
1086     int message_row = (Term->hgt - 23) / 5 + 23;
1087     Term_erase(0, message_row, 255);
1088     put_str("[Choose 'New' or 'Open' from the 'File' menu]",
1089             message_row, (Term->wid - 45) / 2);
1090     Term_fresh();
1091
1092     /*
1093      * Play a game -- "new_game" is set by "new", "open" or the open document
1094      * even handler as appropriate
1095      */
1096         
1097     [pool drain];
1098     
1099     while (!game_in_progress) {
1100         NSAutoreleasePool *splashScreenPool = [[NSAutoreleasePool alloc] init];
1101         NSEvent *event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantFuture] inMode:NSDefaultRunLoopMode dequeue:YES];
1102         if (event) [NSApp sendEvent:event];
1103         [splashScreenPool drain];
1104     }
1105
1106     Term_fresh();
1107     play_game(new_game);
1108
1109     quit(NULL);
1110 }
1111
1112 + (void)endGame
1113 {    
1114     /* Hack -- Forget messages */
1115     msg_flag = FALSE;
1116     
1117     p_ptr->playing = FALSE;
1118     p_ptr->leaving = TRUE;
1119     quit_when_ready = TRUE;
1120 }
1121
1122 - (void)addAngbandView:(AngbandView *)view
1123 {
1124     if (! [angbandViews containsObject:view])
1125     {
1126         [angbandViews addObject:view];
1127         [self updateImage];
1128         [self setNeedsDisplay:YES]; /* We'll need to redisplay everything anyways, so avoid creating all those little redisplay rects */
1129         [self requestRedraw];
1130     }
1131 }
1132
1133 /**
1134  * We have this notion of an "active" AngbandView, which is the largest - the
1135  * idea being that in the screen saver, when the user hits Test in System
1136  * Preferences, we don't want to keep driving the AngbandView in the
1137  * background.  Our active AngbandView is the widest - that's a hack all right.
1138  * Mercifully when we're just playing the game there's only one view.
1139  */
1140 - (AngbandView *)activeView
1141 {
1142     if ([angbandViews count] == 1)
1143         return [angbandViews objectAtIndex:0];
1144     
1145     AngbandView *result = nil;
1146     float maxWidth = 0;
1147     for (AngbandView *angbandView in angbandViews)
1148     {
1149         float width = [angbandView frame].size.width;
1150         if (width > maxWidth)
1151         {
1152             maxWidth = width;
1153             result = angbandView;
1154         }
1155     }
1156     return result;
1157 }
1158
1159 - (void)angbandViewDidScale:(AngbandView *)view
1160 {
1161     /* If we're live-resizing with graphics, we're using the live resize
1162          * optimization, so don't update the image. Otherwise do it. */
1163     if (! (inLiveResize && graphics_are_enabled()) && view == [self activeView])
1164     {
1165         [self updateImage];
1166         
1167         [self setNeedsDisplay:YES]; /*we'll need to redisplay everything anyways, so avoid creating all those little redisplay rects */
1168         [self requestRedraw];
1169     }
1170 }
1171
1172
1173 - (void)removeAngbandView:(AngbandView *)view
1174 {
1175     if ([angbandViews containsObject:view])
1176     {
1177         [angbandViews removeObject:view];
1178         [self updateImage];
1179         [self setNeedsDisplay:YES]; /* We'll need to redisplay everything anyways, so avoid creating all those little redisplay rects */
1180         if ([angbandViews count]) [self requestRedraw];
1181     }
1182 }
1183
1184
1185 static NSMenuItem *superitem(NSMenuItem *self)
1186 {
1187     NSMenu *supermenu = [[self menu] supermenu];
1188     int index = [supermenu indexOfItemWithSubmenu:[self menu]];
1189     if (index == -1) return nil;
1190     else return [supermenu itemAtIndex:index];
1191 }
1192
1193
1194 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
1195 {
1196     int tag = [menuItem tag];
1197     SEL sel = [menuItem action];
1198     if (sel == @selector(setGraphicsMode:))
1199     {
1200         [menuItem setState: (tag == graf_mode_req)];
1201         return YES;
1202     }
1203     else
1204     {
1205         return YES;
1206     }
1207 }
1208
1209 - (NSWindow *)makePrimaryWindow
1210 {
1211     if (! primaryWindow)
1212     {
1213         /* This has to be done after the font is set, which it already is in
1214                  * term_init_cocoa() */
1215         CGFloat width = self->cols * tileSize.width + borderSize.width * 2.0;
1216         CGFloat height = self->rows * tileSize.height + borderSize.height * 2.0;
1217         NSRect contentRect = NSMakeRect( 0.0, 0.0, width, height );
1218
1219         NSUInteger styleMask = NSTitledWindowMask | NSResizableWindowMask | NSMiniaturizableWindowMask;
1220
1221         /* Make every window other than the main window closable */
1222         if( angband_term[0]->data != self )
1223         {
1224             styleMask |= NSClosableWindowMask;
1225         }
1226
1227         primaryWindow = [[NSWindow alloc] initWithContentRect:contentRect styleMask: styleMask backing:NSBackingStoreBuffered defer:YES];
1228
1229         /* Not to be released when closed */
1230         [primaryWindow setReleasedWhenClosed:NO];
1231         [primaryWindow setExcludedFromWindowsMenu: YES]; /* we're using custom window menu handling */
1232
1233         /* Make the view */
1234         AngbandView *angbandView = [[AngbandView alloc] initWithFrame:contentRect];
1235         [angbandView setAngbandContext:self];
1236         [angbandViews addObject:angbandView];
1237         [primaryWindow setContentView:angbandView];
1238         [angbandView release];
1239
1240         /* We are its delegate */
1241         [primaryWindow setDelegate:self];
1242
1243         /* Update our image, since this is probably the first angband view
1244                  * we've gotten. */
1245         [self updateImage];
1246     }
1247     return primaryWindow;
1248 }
1249
1250
1251
1252 #pragma mark View/Window Passthrough
1253
1254 /**
1255  * This is what our views call to get us to draw to the window
1256  */
1257 - (void)drawRect:(NSRect)rect inView:(NSView *)view
1258 {
1259     /* Take this opportunity to throttle so we don't flush faster than desired.
1260          */
1261     BOOL viewInLiveResize = [view inLiveResize];
1262     if (! viewInLiveResize) [self throttle];
1263
1264     /* With a GLayer, use CGContextDrawLayerInRect */
1265     CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort];
1266     NSRect bounds = [view bounds];
1267     if (viewInLiveResize) CGContextSetInterpolationQuality(context, kCGInterpolationLow);
1268     CGContextSetBlendMode(context, kCGBlendModeCopy);
1269     CGContextDrawLayerInRect(context, *(CGRect *)&bounds, angbandLayer);
1270     if (viewInLiveResize) CGContextSetInterpolationQuality(context, kCGInterpolationDefault);
1271 }
1272
1273 - (BOOL)isOrderedIn
1274 {
1275     return [[[angbandViews lastObject] window] isVisible];
1276 }
1277
1278 - (BOOL)isMainWindow
1279 {
1280     return [[[angbandViews lastObject] window] isMainWindow];
1281 }
1282
1283 - (void)setNeedsDisplay:(BOOL)val
1284 {
1285     for (NSView *angbandView in angbandViews)
1286     {
1287         [angbandView setNeedsDisplay:val];
1288     }
1289 }
1290
1291 - (void)setNeedsDisplayInBaseRect:(NSRect)rect
1292 {
1293     for (NSView *angbandView in angbandViews)
1294     {
1295         [angbandView setNeedsDisplayInRect: rect];
1296     }
1297 }
1298
1299 - (void)displayIfNeeded
1300 {
1301     [[self activeView] displayIfNeeded];
1302 }
1303
1304 - (int)terminalIndex
1305 {
1306         int termIndex = 0;
1307
1308         for( termIndex = 0; termIndex < ANGBAND_TERM_MAX; termIndex++ )
1309         {
1310                 if( angband_term[termIndex] == self->terminal )
1311                 {
1312                         break;
1313                 }
1314         }
1315
1316         return termIndex;
1317 }
1318
1319 - (void)resizeTerminalWithContentRect: (NSRect)contentRect saveToDefaults: (BOOL)saveToDefaults
1320 {
1321     CGFloat newRows = floor( (contentRect.size.height - (borderSize.height * 2.0)) / tileSize.height );
1322     CGFloat newColumns = ceil( (contentRect.size.width - (borderSize.width * 2.0)) / tileSize.width );
1323
1324     if (newRows < 1 || newColumns < 1) return;
1325     self->cols = newColumns;
1326     self->rows = newRows;
1327
1328     if( saveToDefaults )
1329     {
1330         int termIndex = [self terminalIndex];
1331         NSArray *terminals = [[NSUserDefaults standardUserDefaults] valueForKey: AngbandTerminalsDefaultsKey];
1332
1333         if( termIndex < (int)[terminals count] )
1334         {
1335             NSMutableDictionary *mutableTerm = [[NSMutableDictionary alloc] initWithDictionary: [terminals objectAtIndex: termIndex]];
1336             [mutableTerm setValue: [NSNumber numberWithUnsignedInt: self->cols] forKey: AngbandTerminalColumnsDefaultsKey];
1337             [mutableTerm setValue: [NSNumber numberWithUnsignedInt: self->rows] forKey: AngbandTerminalRowsDefaultsKey];
1338
1339             NSMutableArray *mutableTerminals = [[NSMutableArray alloc] initWithArray: terminals];
1340             [mutableTerminals replaceObjectAtIndex: termIndex withObject: mutableTerm];
1341
1342             [[NSUserDefaults standardUserDefaults] setValue: mutableTerminals forKey: AngbandTerminalsDefaultsKey];
1343             [mutableTerminals release];
1344             [mutableTerm release];
1345         }
1346         [[NSUserDefaults standardUserDefaults] synchronize];
1347     }
1348
1349     term *old = Term;
1350     Term_activate( self->terminal );
1351     Term_resize( (int)newColumns, (int)newRows);
1352     Term_redraw();
1353     Term_activate( old );
1354 }
1355
1356 - (void)saveWindowVisibleToDefaults: (BOOL)windowVisible
1357 {
1358         int termIndex = [self terminalIndex];
1359         BOOL safeVisibility = (termIndex == 0) ? YES : windowVisible; /* Ensure main term doesn't go away because of these defaults */
1360         NSArray *terminals = [[NSUserDefaults standardUserDefaults] valueForKey: AngbandTerminalsDefaultsKey];
1361
1362         if( termIndex < (int)[terminals count] )
1363         {
1364                 NSMutableDictionary *mutableTerm = [[NSMutableDictionary alloc] initWithDictionary: [terminals objectAtIndex: termIndex]];
1365                 [mutableTerm setValue: [NSNumber numberWithBool: safeVisibility] forKey: AngbandTerminalVisibleDefaultsKey];
1366
1367                 NSMutableArray *mutableTerminals = [[NSMutableArray alloc] initWithArray: terminals];
1368                 [mutableTerminals replaceObjectAtIndex: termIndex withObject: mutableTerm];
1369
1370                 [[NSUserDefaults standardUserDefaults] setValue: mutableTerminals forKey: AngbandTerminalsDefaultsKey];
1371                 [mutableTerminals release];
1372                 [mutableTerm release];
1373         }
1374 }
1375
1376 - (BOOL)windowVisibleUsingDefaults
1377 {
1378         int termIndex = [self terminalIndex];
1379
1380         if( termIndex == 0 )
1381         {
1382                 return YES;
1383         }
1384
1385         NSArray *terminals = [[NSUserDefaults standardUserDefaults] valueForKey: AngbandTerminalsDefaultsKey];
1386         BOOL visible = NO;
1387
1388         if( termIndex < (int)[terminals count] )
1389         {
1390                 NSDictionary *term = [terminals objectAtIndex: termIndex];
1391                 NSNumber *visibleValue = [term valueForKey: AngbandTerminalVisibleDefaultsKey];
1392
1393                 if( visibleValue != nil )
1394                 {
1395                         visible = [visibleValue boolValue];
1396                 }
1397         }
1398
1399         return visible;
1400 }
1401
1402 #pragma mark -
1403 #pragma mark NSWindowDelegate Methods
1404
1405 /*- (void)windowWillStartLiveResize: (NSNotification *)notification
1406
1407 }*/ 
1408
1409 - (void)windowDidEndLiveResize: (NSNotification *)notification
1410 {
1411     NSWindow *window = [notification object];
1412     NSRect contentRect = [window contentRectForFrameRect: [window frame]];
1413     [self resizeTerminalWithContentRect: contentRect saveToDefaults: YES];
1414 }
1415
1416 /*- (NSSize)windowWillResize: (NSWindow *)sender toSize: (NSSize)frameSize
1417 {
1418 } */
1419
1420 - (void)windowDidEnterFullScreen: (NSNotification *)notification
1421 {
1422     NSWindow *window = [notification object];
1423     NSRect contentRect = [window contentRectForFrameRect: [window frame]];
1424     [self resizeTerminalWithContentRect: contentRect saveToDefaults: NO];
1425 }
1426
1427 - (void)windowDidExitFullScreen: (NSNotification *)notification
1428 {
1429     NSWindow *window = [notification object];
1430     NSRect contentRect = [window contentRectForFrameRect: [window frame]];
1431     [self resizeTerminalWithContentRect: contentRect saveToDefaults: NO];
1432 }
1433
1434 - (void)windowDidBecomeMain:(NSNotification *)notification
1435 {
1436     NSWindow *window = [notification object];
1437
1438     if( window != self->primaryWindow )
1439     {
1440         return;
1441     }
1442
1443     int termIndex = [self terminalIndex];
1444     NSMenuItem *item = [[[NSApplication sharedApplication] windowsMenu] itemWithTag: AngbandWindowMenuItemTagBase + termIndex];
1445     [item setState: NSOnState];
1446
1447     if( [[NSFontPanel sharedFontPanel] isVisible] )
1448     {
1449         [[NSFontPanel sharedFontPanel] setPanelFont: [self selectionFont] isMultiple: NO];
1450     }
1451 }
1452
1453 - (void)windowDidResignMain: (NSNotification *)notification
1454 {
1455     NSWindow *window = [notification object];
1456
1457     if( window != self->primaryWindow )
1458     {
1459         return;
1460     }
1461
1462     int termIndex = [self terminalIndex];
1463     NSMenuItem *item = [[[NSApplication sharedApplication] windowsMenu] itemWithTag: AngbandWindowMenuItemTagBase + termIndex];
1464     [item setState: NSOffState];
1465 }
1466
1467 - (void)windowWillClose: (NSNotification *)notification
1468 {
1469         [self saveWindowVisibleToDefaults: NO];
1470 }
1471
1472 @end
1473
1474
1475 @implementation AngbandView
1476
1477 - (BOOL)isOpaque
1478 {
1479     return YES;
1480 }
1481
1482 - (BOOL)isFlipped
1483 {
1484     return YES;
1485 }
1486
1487 - (void)drawRect:(NSRect)rect
1488 {
1489     if (! angbandContext)
1490     {
1491         /* Draw bright orange, 'cause this ain't right */
1492         [[NSColor orangeColor] set];
1493         NSRectFill([self bounds]);
1494     }
1495     else
1496     {
1497         /* Tell the Angband context to draw into us */
1498         [angbandContext drawRect:rect inView:self];
1499     }
1500 }
1501
1502 - (void)setAngbandContext:(AngbandContext *)context
1503 {
1504     angbandContext = context;
1505 }
1506
1507 - (AngbandContext *)angbandContext
1508 {
1509     return angbandContext;
1510 }
1511
1512 - (void)setFrameSize:(NSSize)size
1513 {
1514     BOOL changed = ! NSEqualSizes(size, [self frame].size);
1515     [super setFrameSize:size];
1516     if (changed) [angbandContext angbandViewDidScale:self];
1517 }
1518
1519 - (void)viewWillStartLiveResize
1520 {
1521     [angbandContext viewWillStartLiveResize:self];
1522 }
1523
1524 - (void)viewDidEndLiveResize
1525 {
1526     [angbandContext viewDidEndLiveResize:self];
1527 }
1528
1529 @end
1530
1531 /**
1532  * Delay handling of double-clicked savefiles
1533  */
1534 Boolean open_when_ready = FALSE;
1535
1536
1537
1538 /**
1539  * ------------------------------------------------------------------------
1540  * Some generic functions
1541  * ------------------------------------------------------------------------ */
1542
1543 /**
1544  * Sets an Angband color at a given index
1545  */
1546 static void set_color_for_index(int idx)
1547 {
1548     u16b rv, gv, bv;
1549     
1550     /* Extract the R,G,B data */
1551     rv = angband_color_table[idx][1];
1552     gv = angband_color_table[idx][2];
1553     bv = angband_color_table[idx][3];
1554     
1555     CGContextSetRGBFillColor([[NSGraphicsContext currentContext] graphicsPort], rv/255., gv/255., bv/255., 1.);
1556 }
1557
1558 /**
1559  * Remember the current character in UserDefaults so we can select it by
1560  * default next time.
1561  */
1562 static void record_current_savefile(void)
1563 {
1564     NSString *savefileString = [[NSString stringWithCString:savefile encoding:NSMacOSRomanStringEncoding] lastPathComponent];
1565     if (savefileString)
1566     {
1567         NSUserDefaults *angbandDefs = [NSUserDefaults angbandDefaults];
1568         [angbandDefs setObject:savefileString forKey:@"SaveFile"];
1569         [angbandDefs synchronize];        
1570     }
1571 }
1572
1573
1574 #ifdef JP
1575 /**
1576  * Convert a two-byte EUC-JP encoded character (both *cp and (*cp + 1) are in
1577  * the range, 0xA1-0xFE, or *cp is 0x8E) to a utf16 value in the native byte
1578  * ordering.
1579  */
1580 static wchar_t convert_two_byte_eucjp_to_utf16_native(const char *cp)
1581 {
1582     NSString* str = [[NSString alloc] initWithBytes:cp length:2
1583                                       encoding:NSJapaneseEUCStringEncoding];
1584     wchar_t result = [str characterAtIndex:0];
1585
1586     [str release];
1587     return result;
1588 }
1589 #endif /* JP */
1590
1591
1592 /**
1593  * ------------------------------------------------------------------------
1594  * Support for the "z-term.c" package
1595  * ------------------------------------------------------------------------ */
1596
1597
1598 /**
1599  * Initialize a new Term
1600  */
1601 static void Term_init_cocoa(term *t)
1602 {
1603     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1604     AngbandContext *context = [[AngbandContext alloc] init];
1605     
1606     /* Give the term a hard retain on context (for GC) */
1607     t->data = (void *)CFRetain(context);
1608     [context release];
1609     
1610     /* Handle graphics */
1611     t->higher_pict = !! use_graphics;
1612     t->always_pict = FALSE;
1613     
1614     NSDisableScreenUpdates();
1615     
1616     /* Figure out the frame autosave name based on the index of this term */
1617     NSString *autosaveName = nil;
1618     int termIdx;
1619     for (termIdx = 0; termIdx < ANGBAND_TERM_MAX; termIdx++)
1620     {
1621         if (angband_term[termIdx] == t)
1622         {
1623             autosaveName = [NSString stringWithFormat:@"AngbandTerm-%d", termIdx];
1624             break;
1625         }
1626     }
1627
1628     /* Set its font. */
1629     NSString *fontName = [[NSUserDefaults angbandDefaults] stringForKey:[NSString stringWithFormat:@"FontName-%d", termIdx]];
1630     if (! fontName) fontName = [default_font fontName];
1631
1632     /* Use a smaller default font for the other windows, but only if the font
1633          * hasn't been explicitly set */
1634     float fontSize = (termIdx > 0) ? 10.0 : [default_font pointSize];
1635     NSNumber *fontSizeNumber = [[NSUserDefaults angbandDefaults] valueForKey: [NSString stringWithFormat: @"FontSize-%d", termIdx]];
1636
1637     if( fontSizeNumber != nil )
1638     {
1639         fontSize = [fontSizeNumber floatValue];
1640     }
1641
1642     [context setSelectionFont:[NSFont fontWithName:fontName size:fontSize] adjustTerminal: NO];
1643
1644     NSArray *terminalDefaults = [[NSUserDefaults standardUserDefaults] valueForKey: AngbandTerminalsDefaultsKey];
1645     NSInteger rows = 24;
1646     NSInteger columns = 80;
1647
1648     if( termIdx < (int)[terminalDefaults count] )
1649     {
1650         NSDictionary *term = [terminalDefaults objectAtIndex: termIdx];
1651         NSInteger defaultRows = [[term valueForKey: AngbandTerminalRowsDefaultsKey] integerValue];
1652         NSInteger defaultColumns = [[term valueForKey: AngbandTerminalColumnsDefaultsKey] integerValue];
1653
1654         if (defaultRows > 0) rows = defaultRows;
1655         if (defaultColumns > 0) columns = defaultColumns;
1656     }
1657
1658     context->cols = columns;
1659     context->rows = rows;
1660
1661     /* Get the window */
1662     NSWindow *window = [context makePrimaryWindow];
1663     
1664     /* Set its title and, for auxiliary terms, tentative size */
1665     if (termIdx == 0)
1666     {
1667         [window setTitle:@"Hengband"];
1668
1669         /* Set minimum size (80x24) */
1670         NSSize minsize;
1671         minsize.width = 80 * context->tileSize.width + context->borderSize.width * 2.0;
1672         minsize.height = 24 * context->tileSize.height + context->borderSize.height * 2.0;
1673         [window setContentMinSize:minsize];
1674     }
1675     else
1676     {
1677         [window setTitle:[NSString stringWithFormat:@"Term %d", termIdx]];
1678         /* Set minimum size (1x1) */
1679         NSSize minsize;
1680         minsize.width = context->tileSize.width + context->borderSize.width * 2.0;
1681         minsize.height = context->tileSize.height + context->borderSize.height * 2.0;
1682         [window setContentMinSize:minsize];
1683     }
1684     
1685     
1686     /* If this is the first term, and we support full screen (Mac OS X Lion or
1687          * later), then allow it to go full screen (sweet). Allow other terms to be
1688          * FullScreenAuxilliary, so they can at least show up. Unfortunately in
1689          * Lion they don't get brought to the full screen space; but they would
1690          * only make sense on multiple displays anyways so it's not a big loss. */
1691     if ([window respondsToSelector:@selector(toggleFullScreen:)])
1692     {
1693         NSWindowCollectionBehavior behavior = [window collectionBehavior];
1694         behavior |= (termIdx == 0 ? Angband_NSWindowCollectionBehaviorFullScreenPrimary : Angband_NSWindowCollectionBehaviorFullScreenAuxiliary);
1695         [window setCollectionBehavior:behavior];
1696     }
1697     
1698     /* No Resume support yet, though it would not be hard to add */
1699     if ([window respondsToSelector:@selector(setRestorable:)])
1700     {
1701         [window setRestorable:NO];
1702     }
1703
1704         /* default window placement */ {
1705                 static NSRect overallBoundingRect;
1706
1707                 if( termIdx == 0 )
1708                 {
1709                         /* This is a bit of a trick to allow us to display multiple windows
1710                          * in the "standard default" window position in OS X: the upper
1711                          * center of the screen.
1712                          * The term sizes set in load_prefs() are based on a 5-wide by
1713                          * 3-high grid, with the main term being 4/5 wide by 2/3 high
1714                          * (hence the scaling to find */
1715
1716                         /* What the containing rect would be). */
1717                         NSRect originalMainTermFrame = [window frame];
1718                         NSRect scaledFrame = originalMainTermFrame;
1719                         scaledFrame.size.width *= 5.0 / 4.0;
1720                         scaledFrame.size.height *= 3.0 / 2.0;
1721                         scaledFrame.size.width += 1.0; /* spacing between window columns */
1722                         scaledFrame.size.height += 1.0; /* spacing between window rows */
1723                         [window setFrame: scaledFrame  display: NO];
1724                         [window center];
1725                         overallBoundingRect = [window frame];
1726                         [window setFrame: originalMainTermFrame display: NO];
1727                 }
1728
1729                 static NSRect mainTermBaseRect;
1730                 NSRect windowFrame = [window frame];
1731
1732                 if( termIdx == 0 )
1733                 {
1734                         /* The height and width adjustments were determined experimentally,
1735                          * so that the rest of the windows line up nicely without
1736                          * overlapping */
1737             windowFrame.size.width += 7.0;
1738                         windowFrame.size.height += 9.0;
1739                         windowFrame.origin.x = NSMinX( overallBoundingRect );
1740                         windowFrame.origin.y = NSMaxY( overallBoundingRect ) - NSHeight( windowFrame );
1741                         mainTermBaseRect = windowFrame;
1742                 }
1743                 else if( termIdx == 1 )
1744                 {
1745                         windowFrame.origin.x = NSMinX( mainTermBaseRect );
1746                         windowFrame.origin.y = NSMinY( mainTermBaseRect ) - NSHeight( windowFrame ) - 1.0;
1747                 }
1748                 else if( termIdx == 2 )
1749                 {
1750                         windowFrame.origin.x = NSMaxX( mainTermBaseRect ) + 1.0;
1751                         windowFrame.origin.y = NSMaxY( mainTermBaseRect ) - NSHeight( windowFrame );
1752                 }
1753                 else if( termIdx == 3 )
1754                 {
1755                         windowFrame.origin.x = NSMaxX( mainTermBaseRect ) + 1.0;
1756                         windowFrame.origin.y = NSMinY( mainTermBaseRect ) - NSHeight( windowFrame ) - 1.0;
1757                 }
1758                 else if( termIdx == 4 )
1759                 {
1760                         windowFrame.origin.x = NSMaxX( mainTermBaseRect ) + 1.0;
1761                         windowFrame.origin.y = NSMinY( mainTermBaseRect );
1762                 }
1763                 else if( termIdx == 5 )
1764                 {
1765                         windowFrame.origin.x = NSMinX( mainTermBaseRect ) + NSWidth( windowFrame ) + 1.0;
1766                         windowFrame.origin.y = NSMinY( mainTermBaseRect ) - NSHeight( windowFrame ) - 1.0;
1767                 }
1768
1769                 [window setFrame: windowFrame display: NO];
1770         }
1771
1772         /* Override the default frame above if the user has adjusted windows in
1773          * the past */
1774         if (autosaveName) [window setFrameAutosaveName:autosaveName];
1775
1776     /* Tell it about its term. Do this after we've sized it so that the sizing
1777          * doesn't trigger redrawing and such. */
1778     [context setTerm:t];
1779     
1780     /* Only order front if it's the first term. Other terms will be ordered
1781          * front from AngbandUpdateWindowVisibility(). This is to work around a
1782          * problem where Angband aggressively tells us to initialize terms that
1783          * don't do anything! */
1784     if (t == angband_term[0]) [context->primaryWindow makeKeyAndOrderFront: nil];
1785     
1786     NSEnableScreenUpdates();
1787     
1788     /* Set "mapped" flag */
1789     t->mapped_flag = true;
1790     [pool drain];
1791 }
1792
1793
1794
1795 /**
1796  * Nuke an old Term
1797  */
1798 static void Term_nuke_cocoa(term *t)
1799 {
1800     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1801     
1802     AngbandContext *context = t->data;
1803     if (context)
1804     {
1805         /* Tell the context to get rid of its windows, etc. */
1806         [context dispose];
1807         
1808         /* Balance our CFRetain from when we created it */
1809         CFRelease(context);
1810         
1811         /* Done with it */
1812         t->data = NULL;
1813     }
1814     
1815     [pool drain];
1816 }
1817
1818 /**
1819  * Returns the CGImageRef corresponding to an image with the given name in the
1820  * resource directory, transferring ownership to the caller
1821  */
1822 static CGImageRef create_angband_image(NSString *path)
1823 {
1824     CGImageRef decodedImage = NULL, result = NULL;
1825     
1826     /* Try using ImageIO to load the image */
1827     if (path)
1828     {
1829         NSURL *url = [[NSURL alloc] initFileURLWithPath:path isDirectory:NO];
1830         if (url)
1831         {
1832             NSDictionary *options = [[NSDictionary alloc] initWithObjectsAndKeys:(id)kCFBooleanTrue, kCGImageSourceShouldCache, nil];
1833             CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)url, (CFDictionaryRef)options);
1834             if (source)
1835             {
1836                 /* We really want the largest image, but in practice there's
1837                                  * only going to be one */
1838                 decodedImage = CGImageSourceCreateImageAtIndex(source, 0, (CFDictionaryRef)options);
1839                 CFRelease(source);
1840             }
1841             [options release];
1842             [url release];
1843         }
1844     }
1845     
1846     /* Draw the sucker to defeat ImageIO's weird desire to cache and decode on
1847          * demand. Our images aren't that big! */
1848     if (decodedImage)
1849     {
1850         size_t width = CGImageGetWidth(decodedImage), height = CGImageGetHeight(decodedImage);
1851         
1852         /* Compute our own bitmap info */
1853         CGBitmapInfo imageBitmapInfo = CGImageGetBitmapInfo(decodedImage);
1854         CGBitmapInfo contextBitmapInfo = kCGBitmapByteOrderDefault;
1855         
1856         switch (imageBitmapInfo & kCGBitmapAlphaInfoMask) {
1857             case kCGImageAlphaNone:
1858             case kCGImageAlphaNoneSkipLast:
1859             case kCGImageAlphaNoneSkipFirst:
1860                 /* No alpha */
1861                 contextBitmapInfo |= kCGImageAlphaNone;
1862                 break;
1863             default:
1864                 /* Some alpha, use premultiplied last which is most efficient. */
1865                 contextBitmapInfo |= kCGImageAlphaPremultipliedLast;
1866                 break;
1867         }
1868
1869         /* Draw the source image flipped, since the view is flipped */
1870         CGContextRef ctx = CGBitmapContextCreate(NULL, width, height, CGImageGetBitsPerComponent(decodedImage), CGImageGetBytesPerRow(decodedImage), CGImageGetColorSpace(decodedImage), contextBitmapInfo);
1871         CGContextSetBlendMode(ctx, kCGBlendModeCopy);
1872         CGContextTranslateCTM(ctx, 0.0, height);
1873         CGContextScaleCTM(ctx, 1.0, -1.0);
1874         CGContextDrawImage(ctx, CGRectMake(0, 0, width, height), decodedImage);
1875         result = CGBitmapContextCreateImage(ctx);
1876
1877         /* Done with these things */
1878         CFRelease(ctx);
1879         CGImageRelease(decodedImage);
1880     }
1881     return result;
1882 }
1883
1884 /**
1885  * React to changes
1886  */
1887 static errr Term_xtra_cocoa_react(void)
1888 {
1889     /* Don't actually switch graphics until the game is running */
1890     if (!initialized || !game_in_progress) return (-1);
1891
1892     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1893     AngbandContext *angbandContext = Term->data;
1894
1895     /* Handle graphics */
1896     int expected_graf_mode = (current_graphics_mode) ?
1897         current_graphics_mode->grafID : GRAPHICS_NONE;
1898     if (graf_mode_req != expected_graf_mode)
1899     {
1900         graphics_mode *new_mode;
1901         if (graf_mode_req != GRAPHICS_NONE) {
1902             new_mode = get_graphics_mode(graf_mode_req);
1903         } else {
1904             new_mode = NULL;
1905         }
1906         
1907         /* Get rid of the old image. CGImageRelease is NULL-safe. */
1908         CGImageRelease(pict_image);
1909         pict_image = NULL;
1910         
1911         /* Try creating the image if we want one */
1912         if (new_mode != NULL)
1913         {
1914             NSString *img_path = [NSString stringWithFormat:@"%s/%s", new_mode->path, new_mode->file];
1915             pict_image = create_angband_image(img_path);
1916
1917             /* If we failed to create the image, set the new desired mode to
1918                          * NULL */
1919             if (! pict_image)
1920                 new_mode = NULL;
1921         }
1922         
1923         /* Record what we did */
1924         use_graphics = new_mode ? new_mode->grafID : 0;
1925 #if 0
1926         /* This global is not in Hengband. */
1927         use_transparency = (new_mode != NULL);
1928 #endif
1929         ANGBAND_GRAF = (new_mode ? new_mode->graf : "ascii");
1930         current_graphics_mode = new_mode;
1931         
1932         /* Enable or disable higher picts. Note: this should be done for all
1933                  * terms. */
1934         angbandContext->terminal->higher_pict = !! use_graphics;
1935         
1936         if (pict_image && current_graphics_mode)
1937         {
1938             /* Compute the row and column count via the image height and width.
1939                          */
1940             pict_rows = (int)(CGImageGetHeight(pict_image) / current_graphics_mode->cell_height);
1941             pict_cols = (int)(CGImageGetWidth(pict_image) / current_graphics_mode->cell_width);
1942         }
1943         else
1944         {
1945             pict_rows = 0;
1946             pict_cols = 0;
1947         }
1948         
1949         /* Reset visuals */
1950         if (initialized && game_in_progress)
1951         {
1952             reset_visuals();
1953         }
1954     }
1955     [pool drain];
1956     
1957     /* Success */
1958     return (0);
1959 }
1960
1961
1962 /**
1963  * Do a "special thing"
1964  */
1965 static errr Term_xtra_cocoa(int n, int v)
1966 {
1967     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1968     AngbandContext* angbandContext = Term->data;
1969     
1970     errr result = 0;
1971     
1972     /* Analyze */
1973     switch (n)
1974     {
1975                 /* Make a noise */
1976         case TERM_XTRA_NOISE:
1977         {
1978             NSBeep();
1979             
1980             /* Success */
1981             break;
1982         }
1983
1984         /*  Make a sound */
1985         case TERM_XTRA_SOUND:
1986             play_sound(v);
1987             break;
1988
1989             /* Process random events */
1990         case TERM_XTRA_BORED:
1991         {
1992             /* Show or hide cocoa windows based on the subwindow flags set by
1993                          * the user */
1994             AngbandUpdateWindowVisibility();
1995
1996             /* Process an event */
1997             (void)check_events(CHECK_EVENTS_NO_WAIT);
1998             
1999             /* Success */
2000             break;
2001         }
2002             
2003                 /* Process pending events */
2004         case TERM_XTRA_EVENT:
2005         {
2006             /* Process an event */
2007             (void)check_events(v);
2008             
2009             /* Success */
2010             break;
2011         }
2012             
2013                 /* Flush all pending events (if any) */
2014         case TERM_XTRA_FLUSH:
2015         {
2016             /* Hack -- flush all events */
2017             while (check_events(CHECK_EVENTS_DRAIN)) /* loop */;
2018             
2019             /* Success */
2020             break;
2021         }
2022             
2023                 /* Hack -- Change the "soft level" */
2024         case TERM_XTRA_LEVEL:
2025         {
2026             /* Here we could activate (if requested), but I don't think Angband
2027                          * should be telling us our window order (the user should decide
2028                          * that), so do nothing. */            
2029             break;
2030         }
2031             
2032                 /* Clear the screen */
2033         case TERM_XTRA_CLEAR:
2034         {        
2035             [angbandContext lockFocus];
2036             [[NSColor blackColor] set];
2037             NSRect imageRect = {NSZeroPoint, [angbandContext imageSize]};            
2038             NSRectFillUsingOperation(imageRect, NSCompositeCopy);
2039             [angbandContext unlockFocus];
2040             [angbandContext setNeedsDisplay:YES];
2041             /* Success */
2042             break;
2043         }
2044             
2045                 /* React to changes */
2046         case TERM_XTRA_REACT:
2047         {
2048             /* React to changes */
2049             return (Term_xtra_cocoa_react());
2050         }
2051             
2052                 /* Delay (milliseconds) */
2053         case TERM_XTRA_DELAY:
2054         {
2055             /* If needed */
2056             if (v > 0)
2057             {
2058                 
2059                 double seconds = v / 1000.;
2060                 NSDate* date = [NSDate dateWithTimeIntervalSinceNow:seconds];
2061                 do
2062                 {
2063                     NSEvent* event;
2064                     do
2065                     {
2066                         event = [NSApp nextEventMatchingMask:-1 untilDate:date inMode:NSDefaultRunLoopMode dequeue:YES];
2067                         if (event) send_event(event);
2068                     } while (event);
2069                 } while ([date timeIntervalSinceNow] >= 0);
2070                 
2071             }
2072             
2073             /* Success */
2074             break;
2075         }
2076             
2077         case TERM_XTRA_FRESH:
2078         {
2079             /* No-op -- see #1669 
2080              * [angbandContext displayIfNeeded]; */
2081             break;
2082         }
2083             
2084         default:
2085             /* Oops */
2086             result = 1;
2087             break;
2088     }
2089     
2090     [pool drain];
2091     
2092     /* Oops */
2093     return result;
2094 }
2095
2096 static errr Term_curs_cocoa(int x, int y)
2097 {
2098     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
2099     AngbandContext *angbandContext = Term->data;
2100     
2101     /* Get the tile */
2102     NSRect rect = [angbandContext rectInImageForTileAtX:x Y:y];
2103     
2104     /* We'll need to redisplay in that rect */
2105     NSRect redisplayRect = rect;
2106
2107     /* Go to the pixel boundaries corresponding to this tile */
2108     rect = crack_rect(rect, AngbandScaleIdentity, push_options(x, y));
2109     
2110     /* Lock focus and draw it */
2111     [angbandContext lockFocus];
2112     [[NSColor yellowColor] set];
2113     NSFrameRectWithWidth(rect, 1);
2114     [angbandContext unlockFocus];
2115     
2116     /* Invalidate that rect */
2117     [angbandContext setNeedsDisplayInBaseRect:redisplayRect];
2118     
2119     /* Success */
2120     [pool drain];
2121     return 0;
2122 }
2123
2124 /**
2125  * Low level graphics (Assumes valid input)
2126  *
2127  * Erase "n" characters starting at (x,y)
2128  */
2129 static errr Term_wipe_cocoa(int x, int y, int n)
2130 {
2131     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
2132     AngbandContext *angbandContext = Term->data;
2133     int i;
2134
2135     /*
2136      * Erase the block of characters.  Mimic the geometry calculations for
2137      * the cleared rectangle in Term_text_cocoa().
2138      */
2139     NSRect rect = [angbandContext rectInImageForTileAtX:x Y:y];
2140     rect.size.width = angbandContext->tileSize.width * n;
2141     rect = crack_rect(rect, AngbandScaleIdentity,
2142                       (push_options(x, y) & ~PUSH_LEFT)
2143                       | (push_options(x + n - 1, y) | PUSH_RIGHT));
2144     
2145     /* Lock focus and clear */
2146     [angbandContext lockFocus];
2147     [[NSColor blackColor] set];
2148     NSRectFill(rect);
2149     [angbandContext unlockFocus];    
2150     [angbandContext setNeedsDisplayInBaseRect:rect];
2151     
2152     [pool drain];
2153     
2154     /* Success */
2155     return (0);
2156 }
2157
2158 static void draw_image_tile(CGImageRef image, NSRect srcRect, NSRect dstRect, NSCompositingOperation op)
2159 {
2160     /* Flip the source rect since the source image is flipped */
2161     CGAffineTransform flip = CGAffineTransformIdentity;
2162     flip = CGAffineTransformTranslate(flip, 0.0, CGImageGetHeight(image));
2163     flip = CGAffineTransformScale(flip, 1.0, -1.0);
2164     CGRect flippedSourceRect = CGRectApplyAffineTransform(NSRectToCGRect(srcRect), flip);
2165
2166     /* When we use high-quality resampling to draw a tile, pixels from outside
2167          * the tile may bleed in, causing graphics artifacts. Work around that. */
2168     CGImageRef subimage = CGImageCreateWithImageInRect(image, flippedSourceRect);
2169     NSGraphicsContext *context = [NSGraphicsContext currentContext];
2170     [context setCompositingOperation:op];
2171     CGContextDrawImage([context graphicsPort], NSRectToCGRect(dstRect), subimage);
2172     CGImageRelease(subimage);
2173 }
2174
2175 static errr Term_pict_cocoa(int x, int y, int n, TERM_COLOR *ap,
2176                             const char *cp, const TERM_COLOR *tap,
2177                             const char *tcp)
2178 {
2179     
2180     /* Paranoia: Bail if we don't have a current graphics mode */
2181     if (! current_graphics_mode) return -1;
2182     
2183     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
2184     AngbandContext* angbandContext = Term->data;
2185
2186     /* Lock focus */
2187     [angbandContext lockFocus];
2188     
2189     NSRect destinationRect = [angbandContext rectInImageForTileAtX:x Y:y];
2190
2191     /* Expand the rect to every touching pixel to figure out what to redisplay
2192          */
2193     NSRect redisplayRect = crack_rect(destinationRect, AngbandScaleIdentity, PUSH_RIGHT | PUSH_TOP | PUSH_BOTTOM | PUSH_LEFT);
2194     
2195     /* Expand our destinationRect */
2196     destinationRect = crack_rect(destinationRect, AngbandScaleIdentity, push_options(x, y));
2197     
2198     /* Scan the input */
2199     int i;
2200     int graf_width = current_graphics_mode->cell_width;
2201     int graf_height = current_graphics_mode->cell_height;
2202
2203     for (i = 0; i < n; i++)
2204     {
2205         
2206         TERM_COLOR a = *ap++;
2207         char c = *cp++;
2208         
2209         TERM_COLOR ta = *tap++;
2210         char tc = *tcp++;
2211         
2212         
2213         /* Graphics -- if Available and Needed */
2214         if (use_graphics && (a & 0x80) && (c & 0x80))
2215         {
2216             int col, row;
2217             int t_col, t_row;
2218             
2219
2220             /* Primary Row and Col */
2221             row = ((byte)a & 0x7F) % pict_rows;
2222             col = ((byte)c & 0x7F) % pict_cols;
2223             
2224             NSRect sourceRect;
2225             sourceRect.origin.x = col * graf_width;
2226             sourceRect.origin.y = row * graf_height;
2227             sourceRect.size.width = graf_width;
2228             sourceRect.size.height = graf_height;
2229             
2230             /* Terrain Row and Col */
2231             t_row = ((byte)ta & 0x7F) % pict_rows;
2232             t_col = ((byte)tc & 0x7F) % pict_cols;
2233             
2234             NSRect terrainRect;
2235             terrainRect.origin.x = t_col * graf_width;
2236             terrainRect.origin.y = t_row * graf_height;
2237             terrainRect.size.width = graf_width;
2238             terrainRect.size.height = graf_height;
2239             
2240             /* Transparency effect. We really want to check
2241                          * current_graphics_mode->alphablend, but as of this writing that's
2242                          * never set, so we do something lame.  */
2243             /*if (current_graphics_mode->alphablend) */
2244             if (graf_width > 8 || graf_height > 8)
2245             {
2246                 draw_image_tile(pict_image, terrainRect, destinationRect, NSCompositeCopy);
2247                 draw_image_tile(pict_image, sourceRect, destinationRect, NSCompositeSourceOver); 
2248             }
2249             else
2250             {
2251                 draw_image_tile(pict_image, sourceRect, destinationRect, NSCompositeCopy);
2252             }
2253         }        
2254     }
2255     
2256     [angbandContext unlockFocus];
2257     [angbandContext setNeedsDisplayInBaseRect:redisplayRect];
2258     
2259     [pool drain];
2260     
2261     /* Success */
2262     return (0);
2263 }
2264
2265 /**
2266  * Low level graphics.  Assumes valid input.
2267  *
2268  * Draw several ("n") chars, with an attr, at a given location.
2269  */
2270 static errr Term_text_cocoa(int x, int y, int n, byte_hack a, concptr cp)
2271 {
2272     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
2273     NSRect redisplayRect = NSZeroRect;
2274     AngbandContext* angbandContext = Term->data;
2275     
2276     /* Focus on our layer */
2277     [angbandContext lockFocus];
2278
2279     /* Starting pixel */
2280     NSRect charRect = [angbandContext rectInImageForTileAtX:x Y:y];
2281     
2282     const CGFloat tileWidth = angbandContext->tileSize.width;
2283     
2284     /* erase behind us */
2285     unsigned leftPushOptions = push_options(x, y);
2286     unsigned rightPushOptions = push_options(x + n - 1, y);
2287     leftPushOptions &= ~ PUSH_LEFT;
2288     rightPushOptions |= PUSH_RIGHT;
2289 #if 0    
2290     switch (a / MAX_COLORS) {
2291     case BG_BLACK:
2292             [[NSColor blackColor] set];
2293             break;
2294     case BG_SAME:
2295             set_color_for_index(a % MAX_COLORS);
2296             break;
2297     case BG_DARK:
2298             set_color_for_index(TERM_SHADE);
2299             break;
2300     }
2301 #endif    
2302     NSRect rectToClear = charRect;
2303     rectToClear.size.width = tileWidth * n;
2304     rectToClear = crack_rect(rectToClear, AngbandScaleIdentity,
2305                              leftPushOptions | rightPushOptions);
2306     NSRectFill(rectToClear);
2307     
2308     NSFont *selectionFont = [[angbandContext selectionFont] screenFont];
2309     [selectionFont set];
2310     
2311     /* Set the color */
2312     set_color_for_index(a % MAX_COLORS);
2313     
2314     /* Draw each */
2315     NSRect rectToDraw = charRect;
2316     int i = 0;
2317     while (i < n) {
2318 #ifdef JP
2319         if (iskanji(cp[i])) {
2320             CGFloat w = rectToDraw.size.width;
2321             wchar_t uv = convert_two_byte_eucjp_to_utf16_native(cp + i);
2322
2323             rectToDraw.size.width *= 2.0;
2324             [angbandContext drawWChar:uv inRect:rectToDraw];
2325             rectToDraw.origin.x += tileWidth + tileWidth;
2326             rectToDraw.size.width = w;
2327             i += 2;
2328         } else {
2329             [angbandContext drawWChar:cp[i] inRect:rectToDraw];
2330             rectToDraw.origin.x += tileWidth;
2331             ++i;
2332         }
2333 #else
2334         [angbandContext drawWChar:cp[i] inRect:rectToDraw];
2335         rectToDraw.origin.x += tileWidth;
2336     }
2337
2338     [angbandContext unlockFocus];    
2339     /* Invalidate what we just drew */    
2340     [angbandContext setNeedsDisplayInBaseRect:rectToClear];
2341     
2342     [pool drain];
2343     
2344     /* Success */
2345     return (0);
2346 }
2347
2348 /**
2349  * Post a nonsense event so that our event loop wakes up
2350  */
2351 static void wakeup_event_loop(void)
2352 {
2353     /* Big hack - send a nonsense event to make us update */
2354     NSEvent *event = [NSEvent otherEventWithType:NSApplicationDefined location:NSZeroPoint modifierFlags:0 timestamp:0 windowNumber:0 context:NULL subtype:AngbandEventWakeup data1:0 data2:0];
2355     [NSApp postEvent:event atStart:NO];
2356 }
2357
2358
2359 /**
2360  * Create and initialize window number "i"
2361  */
2362 static term *term_data_link(int i)
2363 {
2364     NSArray *terminalDefaults = [[NSUserDefaults standardUserDefaults] valueForKey: AngbandTerminalsDefaultsKey];
2365     NSInteger rows = 24;
2366     NSInteger columns = 80;
2367
2368     if( i < (int)[terminalDefaults count] )
2369     {
2370         NSDictionary *term = [terminalDefaults objectAtIndex: i];
2371         rows = [[term valueForKey: AngbandTerminalRowsDefaultsKey] integerValue];
2372         columns = [[term valueForKey: AngbandTerminalColumnsDefaultsKey] integerValue];
2373     }
2374
2375     /* Allocate */
2376     term *newterm = ZNEW(term);
2377
2378     /* Initialize the term */
2379     term_init(newterm, columns, rows, 256 /* keypresses, for some reason? */);
2380     
2381     /* Differentiate between BS/^h, Tab/^i, etc. */
2382     /* newterm->complex_input = TRUE; */
2383
2384     /* Use a "software" cursor */
2385     newterm->soft_cursor = TRUE;
2386     
2387     /* Erase with "white space" */
2388     newterm->attr_blank = TERM_WHITE;
2389     newterm->char_blank = ' ';
2390     
2391     /* Prepare the init/nuke hooks */
2392     newterm->init_hook = Term_init_cocoa;
2393     newterm->nuke_hook = Term_nuke_cocoa;
2394     
2395     /* Prepare the function hooks */
2396     newterm->xtra_hook = Term_xtra_cocoa;
2397     newterm->wipe_hook = Term_wipe_cocoa;
2398     newterm->curs_hook = Term_curs_cocoa;
2399     newterm->text_hook = Term_text_cocoa;
2400     newterm->pict_hook = Term_pict_cocoa;
2401     /* newterm->mbcs_hook = Term_mbcs_cocoa; */
2402     
2403     /* Global pointer */
2404     angband_term[i] = newterm;
2405     
2406     return newterm;
2407 }
2408
2409 /**
2410  * Load preferences from preferences file for current host+current user+
2411  * current application.
2412  */
2413 static void load_prefs()
2414 {
2415     NSUserDefaults *defs = [NSUserDefaults angbandDefaults];
2416     
2417     /* Make some default defaults */
2418     NSMutableArray *defaultTerms = [[NSMutableArray alloc] init];
2419
2420     /* The following default rows/cols were determined experimentally by first
2421          * finding the ideal window/font size combinations. But because of awful
2422          * temporal coupling in Term_init_cocoa(), it's impossible to set up the
2423          * defaults there, so we do it this way. */
2424     for( NSUInteger i = 0; i < ANGBAND_TERM_MAX; i++ )
2425     {
2426                 int columns, rows;
2427                 BOOL visible = YES;
2428
2429                 switch( i )
2430                 {
2431                         case 0:
2432                                 columns = 129;
2433                                 rows = 32;
2434                                 break;
2435                         case 1:
2436                                 columns = 84;
2437                                 rows = 20;
2438                                 break;
2439                         case 2:
2440                                 columns = 42;
2441                                 rows = 24;
2442                                 break;
2443                         case 3:
2444                                 columns = 42;
2445                                 rows = 20;
2446                                 break;
2447                         case 4:
2448                                 columns = 42;
2449                                 rows = 16;
2450                                 break;
2451                         case 5:
2452                                 columns = 84;
2453                                 rows = 20;
2454                                 break;
2455                         default:
2456                                 columns = 80;
2457                                 rows = 24;
2458                                 visible = NO;
2459                                 break;
2460                 }
2461
2462                 NSDictionary *standardTerm = [NSDictionary dictionaryWithObjectsAndKeys:
2463                                                                           [NSNumber numberWithInt: rows], AngbandTerminalRowsDefaultsKey,
2464                                                                           [NSNumber numberWithInt: columns], AngbandTerminalColumnsDefaultsKey,
2465                                                                           [NSNumber numberWithBool: visible], AngbandTerminalVisibleDefaultsKey,
2466                                                                           nil];
2467         [defaultTerms addObject: standardTerm];
2468     }
2469
2470     NSDictionary *defaults = [[NSDictionary alloc] initWithObjectsAndKeys:
2471 #ifdef JP
2472                               @"Osaka", @"FontName",
2473 #else
2474                               @"Menlo", @"FontName",
2475 #endif
2476                               [NSNumber numberWithFloat:13.f], @"FontSize",
2477                               [NSNumber numberWithInt:60], AngbandFrameRateDefaultsKey,
2478                               [NSNumber numberWithBool:YES], AngbandSoundDefaultsKey,
2479                               [NSNumber numberWithInt:GRAPHICS_NONE], AngbandGraphicsDefaultsKey,
2480                               defaultTerms, AngbandTerminalsDefaultsKey,
2481                               nil];
2482     [defs registerDefaults:defaults];
2483     [defaults release];
2484     [defaultTerms release];
2485     
2486     /* Preferred graphics mode */
2487     graf_mode_req = [defs integerForKey:AngbandGraphicsDefaultsKey];
2488     
2489     /* Use sounds; set the Angband global */
2490     use_sound = ([defs boolForKey:AngbandSoundDefaultsKey] == YES) ? TRUE : FALSE;
2491     
2492     /* fps */
2493     frames_per_second = [defs integerForKey:AngbandFrameRateDefaultsKey];
2494     
2495     /* Font */
2496     default_font = [[NSFont fontWithName:[defs valueForKey:@"FontName-0"] size:[defs floatForKey:@"FontSize-0"]] retain];
2497     if (! default_font) default_font = [[NSFont fontWithName:@"Menlo" size:13.] retain];
2498 }
2499
2500 /**
2501  * Arbitary limit on number of possible samples per event
2502  */
2503 #define MAX_SAMPLES            16
2504
2505 /**
2506  * Struct representing all data for a set of event samples
2507  */
2508 typedef struct
2509 {
2510         int num;        /* Number of available samples for this event */
2511         NSSound *sound[MAX_SAMPLES];
2512 } sound_sample_list;
2513
2514 /**
2515  * Array of event sound structs
2516  */
2517 static sound_sample_list samples[MSG_MAX];
2518
2519
2520 /**
2521  * Load sound effects based on sound.cfg within the xtra/sound directory;
2522  * bridge to Cocoa to use NSSound for simple loading and playback, avoiding
2523  * I/O latency by cacheing all sounds at the start.  Inherits full sound
2524  * format support from Quicktime base/plugins.
2525  * pelpel favoured a plist-based parser for the future but .cfg support
2526  * improves cross-platform compatibility.
2527  */
2528 static void load_sounds(void)
2529 {
2530         char sound_dir[1024];
2531         char path[1024];
2532         char buffer[2048];
2533         FILE *fff;
2534     
2535         /* Build the "sound" path */
2536         path_build(sound_dir, sizeof(sound_dir), ANGBAND_DIR_XTRA, "sound");
2537     
2538         /* Find and open the config file */
2539         path_build(path, sizeof(path), sound_dir, "sound.cfg");
2540         fff = my_fopen(path, "r");
2541     
2542         /* Handle errors */
2543         if (!fff)
2544         {
2545                 NSLog(@"The sound configuration file could not be opened.");
2546                 return;
2547         }
2548         
2549         /* Instantiate an autorelease pool for use by NSSound */
2550         NSAutoreleasePool *autorelease_pool;
2551         autorelease_pool = [[NSAutoreleasePool alloc] init];
2552     
2553     /* Use a dictionary to unique sounds, so we can share NSSounds across
2554          * multiple events */
2555     NSMutableDictionary *sound_dict = [NSMutableDictionary dictionary];
2556     
2557         /*
2558          * This loop may take a while depending on the count and size of samples
2559          * to load.
2560          */
2561     
2562         /* Parse the file */
2563         /* Lines are always of the form "name = sample [sample ...]" */
2564         while (my_fgets(fff, buffer, sizeof(buffer)) == 0)
2565         {
2566                 char *msg_name;
2567                 char *cfg_sample_list;
2568                 char *search;
2569                 char *cur_token;
2570                 char *next_token;
2571                 int event;
2572         
2573                 /* Skip anything not beginning with an alphabetic character */
2574                 if (!buffer[0] || !isalpha((unsigned char)buffer[0])) continue;
2575         
2576                 /* Split the line into two: message name, and the rest */
2577                 search = strchr(buffer, ' ');
2578                 cfg_sample_list = strchr(search + 1, ' ');
2579                 if (!search) continue;
2580                 if (!cfg_sample_list) continue;
2581         
2582                 /* Set the message name, and terminate at first space */
2583                 msg_name = buffer;
2584                 search[0] = '\0';
2585         
2586                 /* Make sure this is a valid event name */
2587                 for (event = MSG_MAX - 1; event >= 0; event--)
2588                 {
2589                         if (strcmp(msg_name, angband_sound_name[event]) == 0)
2590                                 break;
2591                 }
2592                 if (event < 0) continue;
2593         
2594                 /* Advance the sample list pointer so it's at the beginning of text */
2595                 cfg_sample_list++;
2596                 if (!cfg_sample_list[0]) continue;
2597         
2598                 /* Terminate the current token */
2599                 cur_token = cfg_sample_list;
2600                 search = strchr(cur_token, ' ');
2601                 if (search)
2602                 {
2603                         search[0] = '\0';
2604                         next_token = search + 1;
2605                 }
2606                 else
2607                 {
2608                         next_token = NULL;
2609                 }
2610         
2611                 /*
2612                  * Now we find all the sample names and add them one by one
2613                  */
2614                 while (cur_token)
2615                 {
2616                         int num = samples[event].num;
2617             
2618                         /* Don't allow too many samples */
2619                         if (num >= MAX_SAMPLES) break;
2620             
2621             NSString *token_string = [NSString stringWithUTF8String:cur_token];
2622             NSSound *sound = [sound_dict objectForKey:token_string];
2623             
2624             if (! sound)
2625             {
2626                 struct stat stb;
2627
2628                 /* We have to load the sound. Build the path to the sample */
2629                 path_build(path, sizeof(path), sound_dir, cur_token);
2630                 if (stat(path, &stb) == 0)
2631                 {
2632                     
2633                     /* Load the sound into memory */
2634                     sound = [[[NSSound alloc] initWithContentsOfFile:[NSString stringWithUTF8String:path] byReference:YES] autorelease];
2635                     if (sound) [sound_dict setObject:sound forKey:token_string];
2636                 }
2637             }
2638             
2639             /* Store it if we loaded it */
2640             if (sound)
2641             {
2642                 samples[event].sound[num] = [sound retain];
2643                 
2644                 /* Imcrement the sample count */
2645                 samples[event].num++;
2646             }
2647             
2648             
2649                         /* Figure out next token */
2650                         cur_token = next_token;
2651                         if (next_token)
2652                         {
2653                                 /* Try to find a space */
2654                                 search = strchr(cur_token, ' ');
2655                 
2656                                 /* If we can find one, terminate, and set new "next" */
2657                                 if (search)
2658                                 {
2659                                         search[0] = '\0';
2660                                         next_token = search + 1;
2661                                 }
2662                                 else
2663                                 {
2664                                         /* Otherwise prevent infinite looping */
2665                                         next_token = NULL;
2666                                 }
2667                         }
2668                 }
2669         }
2670     
2671         /* Release the autorelease pool */
2672         [autorelease_pool release];
2673     
2674         /* Close the file */
2675         my_fclose(fff);
2676 }
2677
2678 /**
2679  * Play sound effects asynchronously.  Select a sound from any available
2680  * for the required event, and bridge to Cocoa to play it.
2681  */
2682 static void play_sound(int event)
2683 {    
2684         /* Paranoia */
2685         if (event < 0 || event >= MSG_MAX) return;
2686     
2687     /* Load sounds just-in-time (once) */
2688     static BOOL loaded = NO;
2689     if (!loaded) {
2690         loaded = YES;
2691         load_sounds();
2692     }
2693     
2694     /* Check there are samples for this event */
2695     if (!samples[event].num) return;
2696     
2697     /* Instantiate an autorelease pool for use by NSSound */
2698     NSAutoreleasePool *autorelease_pool;
2699     autorelease_pool = [[NSAutoreleasePool alloc] init];
2700     
2701     /* Choose a random event */
2702     int s = randint0(samples[event].num);
2703     
2704     /* Stop the sound if it's currently playing */
2705     if ([samples[event].sound[s] isPlaying])
2706         [samples[event].sound[s] stop];
2707     
2708     /* Play the sound */
2709     [samples[event].sound[s] play];
2710     
2711     /* Release the autorelease pool */
2712     [autorelease_pool drain];
2713 }
2714
2715 /*
2716  * 
2717  */
2718 static void init_windows(void)
2719 {
2720     /* Create the main window */
2721     term *primary = term_data_link(0);
2722     
2723     /* Prepare to create any additional windows */
2724     int i;
2725     for (i=1; i < ANGBAND_TERM_MAX; i++) {
2726         term_data_link(i);
2727     }
2728     
2729     /* Activate the primary term */
2730     Term_activate(primary);
2731 }
2732
2733 /**
2734  * Handle the "open_when_ready" flag
2735  */
2736 static void handle_open_when_ready(void)
2737 {
2738     /* Check the flag XXX XXX XXX make a function for this */
2739     if (open_when_ready && initialized && !game_in_progress)
2740     {
2741         /* Forget */
2742         open_when_ready = FALSE;
2743         
2744         /* Game is in progress */
2745         game_in_progress = TRUE;
2746         
2747         /* Wait for a keypress */
2748         pause_line(23);
2749     }
2750 }
2751
2752
2753 /**
2754  * Handle quit_when_ready, by Peter Ammon,
2755  * slightly modified to check inkey_flag.
2756  */
2757 static void quit_calmly(void)
2758 {
2759     /* Quit immediately if game's not started */
2760     if (!game_in_progress || !character_generated) quit(NULL);
2761
2762     /* Save the game and Quit (if it's safe) */
2763     if (inkey_flag)
2764     {
2765         /* Hack -- Forget messages and term */
2766         msg_flag = FALSE;
2767                 Term->mapped_flag = FALSE;
2768
2769         /* Save the game */
2770         do_cmd_save_game(FALSE);
2771         record_current_savefile();
2772         
2773         
2774         /* Quit */
2775         quit(NULL);
2776     }
2777
2778     /* Wait until inkey_flag is set */
2779 }
2780
2781
2782
2783 /**
2784  * Returns YES if we contain an AngbandView (and hence should direct our events
2785  * to Angband)
2786  */
2787 static BOOL contains_angband_view(NSView *view)
2788 {
2789     if ([view isKindOfClass:[AngbandView class]]) return YES;
2790     for (NSView *subview in [view subviews]) {
2791         if (contains_angband_view(subview)) return YES;
2792     }
2793     return NO;
2794 }
2795
2796
2797 /**
2798  * Queue mouse presses if they occur in the map section of the main window.
2799  */
2800 static void AngbandHandleEventMouseDown( NSEvent *event )
2801 {
2802 #if 0
2803         AngbandContext *angbandContext = [[[event window] contentView] angbandContext];
2804         AngbandContext *mainAngbandContext = angband_term[0]->data;
2805
2806         if (mainAngbandContext->primaryWindow && [[event window] windowNumber] == [mainAngbandContext->primaryWindow windowNumber])
2807         {
2808                 int cols, rows, x, y;
2809                 Term_get_size(&cols, &rows);
2810                 NSSize tileSize = angbandContext->tileSize;
2811                 NSSize border = angbandContext->borderSize;
2812                 NSPoint windowPoint = [event locationInWindow];
2813
2814                 /* Adjust for border; add border height because window origin is at
2815                  * bottom */
2816                 windowPoint = NSMakePoint( windowPoint.x - border.width, windowPoint.y + border.height );
2817
2818                 NSPoint p = [[[event window] contentView] convertPoint: windowPoint fromView: nil];
2819                 x = floor( p.x / tileSize.width );
2820                 y = floor( p.y / tileSize.height );
2821
2822                 /* Being safe about this, since xcode doesn't seem to like the
2823                  * bool_hack stuff */
2824                 BOOL displayingMapInterface = ((int)inkey_flag != 0);
2825
2826                 /* Sidebar plus border == thirteen characters; top row is reserved. */
2827                 /* Coordinates run from (0,0) to (cols-1, rows-1). */
2828                 BOOL mouseInMapSection = (x > 13 && x <= cols - 1 && y > 0  && y <= rows - 2);
2829
2830                 /* If we are displaying a menu, allow clicks anywhere; if we are
2831                  * displaying the main game interface, only allow clicks in the map
2832                  * section */
2833                 if (!displayingMapInterface || (displayingMapInterface && mouseInMapSection))
2834                 {
2835                         /* [event buttonNumber] will return 0 for left click,
2836                          * 1 for right click, but this is safer */
2837                         int button = ([event type] == NSLeftMouseDown) ? 1 : 2;
2838
2839 #ifdef KC_MOD_ALT
2840                         NSUInteger eventModifiers = [event modifierFlags];
2841                         byte angbandModifiers = 0;
2842                         angbandModifiers |= (eventModifiers & NSShiftKeyMask) ? KC_MOD_SHIFT : 0;
2843                         angbandModifiers |= (eventModifiers & NSControlKeyMask) ? KC_MOD_CONTROL : 0;
2844                         angbandModifiers |= (eventModifiers & NSAlternateKeyMask) ? KC_MOD_ALT : 0;
2845                         button |= (angbandModifiers & 0x0F) << 4; /* encode modifiers in the button number (see Term_mousepress()) */
2846 #endif
2847
2848                         Term_mousepress(x, y, button);
2849                 }
2850         }
2851 #endif
2852     /* Pass click through to permit focus change, resize, etc. */
2853     [NSApp sendEvent:event];
2854 }
2855
2856
2857
2858 /**
2859  * Encodes an NSEvent Angband-style, or forwards it along.  Returns YES if the
2860  * event was sent to Angband, NO if Cocoa (or nothing) handled it */
2861 static BOOL send_event(NSEvent *event)
2862 {
2863
2864     /* If the receiving window is not an Angband window, then do nothing */
2865     if (! contains_angband_view([[event window] contentView]))
2866     {
2867         [NSApp sendEvent:event];
2868         return NO;
2869     }
2870
2871     /* Analyze the event */
2872     switch ([event type])
2873     {
2874         case NSKeyDown:
2875         {
2876             /* Try performing a key equivalent */
2877             if ([[NSApp mainMenu] performKeyEquivalent:event]) break;
2878             
2879             unsigned modifiers = [event modifierFlags];
2880             
2881             /* Send all NSCommandKeyMasks through */
2882             if (modifiers & NSCommandKeyMask)
2883             {
2884                 [NSApp sendEvent:event];
2885                 break;
2886             }
2887             
2888             if (! [[event characters] length]) break;
2889             
2890             
2891             /* Extract some modifiers */
2892 #if 0
2893             /* Caught above so don't do anything with it here. */
2894             int mx = !! (modifiers & NSCommandKeyMask);
2895 #endif            
2896             int mc = !! (modifiers & NSControlKeyMask);
2897             int ms = !! (modifiers & NSShiftKeyMask);
2898             int mo = !! (modifiers & NSAlternateKeyMask);
2899             int kp = !! (modifiers & NSNumericPadKeyMask);
2900             
2901             
2902             /* Get the Angband char corresponding to this unichar */
2903             unichar c = [[event characters] characterAtIndex:0];
2904             char ch;
2905             switch (c) {
2906                 /*
2907                  * Convert some special keys to what would be the normal
2908                  * alternative in the original keyset or, for things lke
2909                  * Delete, Return, and Escape, what one might use from ASCII.
2910                  * The rest of Hengband uses Angband 2.7's or so key handling:
2911                  * so for the rest do something like the encoding that
2912                  * main-win.c does:  send a macro trigger with the Unicode
2913                  * value encoded into printable ASCII characters.  Since
2914                  * macro triggers appear to assume at most two keys plus the
2915                  * modifiers, can only handle values of c below 4096 with
2916                  * 64 values per key.
2917                  */
2918                 case NSUpArrowFunctionKey: ch = '8'; kp = 0; break;
2919                 case NSDownArrowFunctionKey: ch = '2'; kp = 0; break;
2920                 case NSLeftArrowFunctionKey: ch = '4'; kp = 0; break;
2921                 case NSRightArrowFunctionKey: ch = '6'; kp = 0; break;
2922                 case NSHelpFunctionKey: ch = '?'; break;
2923                 case NSDeleteFunctionKey: ch = '\b'; break;
2924                     
2925                 default:
2926                     if (c <= 0x7F)
2927                         ch = (char)c;
2928                     else
2929                         ch = '\0';
2930                     break;
2931             }
2932             
2933             /* override special keys */
2934             switch([event keyCode]) {
2935                 case kVK_Return: ch = '\r'; break;
2936                 case kVK_Escape: ch = 27; break;
2937                 case kVK_Tab: ch = '\t'; break;
2938                 case kVK_Delete: ch = '\b'; break;
2939                 case kVK_ANSI_KeypadEnter: ch = '\r'; kp = TRUE; break;
2940             }
2941
2942             /* Hide the mouse pointer */
2943             [NSCursor setHiddenUntilMouseMoves:YES];
2944             
2945             /* Enqueue it */
2946             if (ch != '\0')
2947             {
2948                 
2949                 /* Enqueue the keypress */
2950 #if 0
2951                 byte mods = 0;
2952                 if (mo) mods |= KC_MOD_ALT;
2953                 if (mx) mods |= KC_MOD_META;
2954                 if (mc && MODS_INCLUDE_CONTROL(ch)) mods |= KC_MOD_CONTROL;
2955                 if (ms && MODS_INCLUDE_SHIFT(ch)) mods |= KC_MOD_SHIFT;
2956                 if (kp) mods |= KC_MOD_KEYPAD;
2957                 Term_keypress(ch, mods);
2958 #else
2959                 Term_keypress(ch);
2960 #endif
2961             } else if (c < 4096 || (c >= 0xF700 && c <= 0xF77F)) {
2962                 unichar part;
2963                 char cenc;
2964
2965                 /* Begin the macro trigger. */
2966                 Term_keypress(31);
2967
2968                 /* Send the modifiers. */
2969                 if (mc) Term_keypress('C');
2970                 if (ms) Term_keypress('S');
2971                 if (mo) Term_keypress('A');
2972                 if (kp) Term_keypress('K');
2973
2974                 /*
2975                  * Put part of the range Apple reserves for special keys
2976                  * into 0 - 127 since that range has been handled normally.
2977                  */
2978                 if (c >= 0xF700) {
2979                     c -= 0xF700;
2980                 }
2981
2982                 /* Encode the value as two printable characters. */
2983                 part = (c >> 6) & 63;
2984                 if (part > 38) {
2985                     cenc = 'a' + (part - 38);
2986                 } else if (part > 12) {
2987                     cenc = 'A' + (part - 12);
2988                 } else {
2989                     cenc = '0' + part;
2990                 }
2991                 Term_keypress(cenc);
2992                 part = c & 63;
2993                 if (part > 38) {
2994                     cenc = 'a' + (part - 38);
2995                 } else if (part > 12) {
2996                     cenc = 'A' + (part - 12);
2997                 } else {
2998                     cenc = '0' + part;
2999                 }
3000                 Term_keypress(cenc);
3001
3002                 /* End the macro trigger. */
3003                 Term_keypress(13);
3004             }
3005             
3006             break;
3007         }
3008             
3009         case NSLeftMouseDown:
3010                 case NSRightMouseDown:
3011                         AngbandHandleEventMouseDown(event);
3012             break;
3013
3014         case NSApplicationDefined:
3015         {
3016             if ([event subtype] == AngbandEventWakeup)
3017             {
3018                 return YES;
3019             }
3020             break;
3021         }
3022             
3023         default:
3024             [NSApp sendEvent:event];
3025             return YES;
3026     }
3027     return YES;
3028 }
3029
3030 /**
3031  * Check for Events, return TRUE if we process any
3032  */
3033 static BOOL check_events(int wait)
3034
3035     
3036     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
3037     
3038     /* Handles the quit_when_ready flag */
3039     if (quit_when_ready) quit_calmly();
3040     
3041     NSDate* endDate;
3042     if (wait == CHECK_EVENTS_WAIT) endDate = [NSDate distantFuture];
3043     else endDate = [NSDate distantPast];
3044     
3045     NSEvent* event;
3046     for (;;) {
3047         if (quit_when_ready)
3048         {
3049             /* send escape events until we quit */
3050             Term_keypress(0x1B);
3051             [pool drain];
3052             return false;
3053         }
3054         else {
3055             event = [NSApp nextEventMatchingMask:-1 untilDate:endDate inMode:NSDefaultRunLoopMode dequeue:YES];
3056             if (! event)
3057             {
3058                 [pool drain];
3059                 return FALSE;
3060             }
3061             if (send_event(event)) break;
3062         }
3063     }
3064     
3065     [pool drain];
3066     
3067     /* Something happened */
3068     return YES;
3069     
3070 }
3071
3072 /**
3073  * Hook to tell the user something important
3074  */
3075 static void hook_plog(const char * str)
3076 {
3077     if (str)
3078     {
3079         NSString *string = [NSString stringWithCString:str encoding:NSMacOSRomanStringEncoding];
3080         NSRunAlertPanel(@"Danger Will Robinson", @"%@", @"OK", nil, nil, string);
3081     }
3082 }
3083
3084
3085 /**
3086  * Hook to tell the user something, and then quit
3087  */
3088 static void hook_quit(const char * str)
3089 {
3090     plog(str);
3091     exit(0);
3092 }
3093
3094 /**
3095  * ------------------------------------------------------------------------
3096  * Main program
3097  * ------------------------------------------------------------------------ */
3098
3099 @interface AngbandAppDelegate : NSObject {
3100     IBOutlet NSMenu *terminalsMenu;
3101     NSMenu *_graphicsMenu;
3102     NSMenu *_commandMenu;
3103     NSDictionary *_commandMenuTagMap;
3104 }
3105
3106 @property (nonatomic, retain) IBOutlet NSMenu *graphicsMenu;
3107 @property (nonatomic, retain) IBOutlet NSMenu *commandMenu;
3108 @property (nonatomic, retain) NSDictionary *commandMenuTagMap;
3109
3110 - (IBAction)newGame:sender;
3111 - (IBAction)openGame:sender;
3112
3113 - (IBAction)editFont:sender;
3114 - (IBAction)setGraphicsMode:(NSMenuItem *)sender;
3115 - (IBAction)toggleSound:(NSMenuItem *)sender;
3116
3117 - (IBAction)setRefreshRate:(NSMenuItem *)menuItem;
3118 - (IBAction)selectWindow: (id)sender;
3119
3120 @end
3121
3122 @implementation AngbandAppDelegate
3123
3124 @synthesize graphicsMenu=_graphicsMenu;
3125 @synthesize commandMenu=_commandMenu;
3126 @synthesize commandMenuTagMap=_commandMenuTagMap;
3127
3128 - (IBAction)newGame:sender
3129 {
3130     /* Game is in progress */
3131     game_in_progress = TRUE;
3132     new_game = TRUE;
3133 }
3134
3135 - (IBAction)editFont:sender
3136 {
3137     NSFontPanel *panel = [NSFontPanel sharedFontPanel];
3138     NSFont *termFont = default_font;
3139
3140     int i;
3141     for (i=0; i < ANGBAND_TERM_MAX; i++) {
3142         if ([(id)angband_term[i]->data isMainWindow]) {
3143             termFont = [(id)angband_term[i]->data selectionFont];
3144             break;
3145         }
3146     }
3147     
3148     [panel setPanelFont:termFont isMultiple:NO];
3149     [panel orderFront:self];
3150 }
3151
3152 /**
3153  * Implent NSObject's changeFont() method to receive a notification about the
3154  * changed font.  Note that, as of 10.14, changeFont() is deprecated in
3155  * NSObject - it will be removed at some point and the application delegate
3156  * will have to be declared as implementing the NSFontChanging protocol.
3157  */
3158 - (void)changeFont:(id)sender
3159 {
3160     int mainTerm;
3161     for (mainTerm=0; mainTerm < ANGBAND_TERM_MAX; mainTerm++) {
3162         if ([(id)angband_term[mainTerm]->data isMainWindow]) {
3163             break;
3164         }
3165     }
3166
3167     /* Bug #1709: Only change font for angband windows */
3168     if (mainTerm == ANGBAND_TERM_MAX) return;
3169     
3170     NSFont *oldFont = default_font;
3171     NSFont *newFont = [sender convertFont:oldFont];
3172     if (! newFont) return; /*paranoia */
3173     
3174     /* Store as the default font if we changed the first term */
3175     if (mainTerm == 0) {
3176         [newFont retain];
3177         [default_font release];
3178         default_font = newFont;
3179     }
3180     
3181     /* Record it in the preferences */
3182     NSUserDefaults *defs = [NSUserDefaults angbandDefaults];
3183     [defs setValue:[newFont fontName] 
3184         forKey:[NSString stringWithFormat:@"FontName-%d", mainTerm]];
3185     [defs setFloat:[newFont pointSize]
3186         forKey:[NSString stringWithFormat:@"FontSize-%d", mainTerm]];
3187     [defs synchronize];
3188     
3189     NSDisableScreenUpdates();
3190     
3191     /* Update window */
3192     AngbandContext *angbandContext = angband_term[mainTerm]->data;
3193     [(id)angbandContext setSelectionFont:newFont adjustTerminal: YES];
3194     
3195     NSEnableScreenUpdates();
3196 }
3197
3198 - (IBAction)openGame:sender
3199 {
3200     NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
3201     BOOL selectedSomething = NO;
3202     int panelResult;
3203     
3204     /* Get where we think the save files are */
3205     NSURL *startingDirectoryURL = [NSURL fileURLWithPath:[NSString stringWithCString:ANGBAND_DIR_SAVE encoding:NSASCIIStringEncoding] isDirectory:YES];
3206     
3207     /* Set up an open panel */
3208     NSOpenPanel* panel = [NSOpenPanel openPanel];
3209     [panel setCanChooseFiles:YES];
3210     [panel setCanChooseDirectories:NO];
3211     [panel setResolvesAliases:YES];
3212     [panel setAllowsMultipleSelection:NO];
3213     [panel setTreatsFilePackagesAsDirectories:YES];
3214     [panel setDirectoryURL:startingDirectoryURL];
3215     
3216     /* Run it */
3217     panelResult = [panel runModal];
3218     if (panelResult == NSOKButton)
3219     {
3220         NSArray* fileURLs = [panel URLs];
3221         if ([fileURLs count] > 0 && [[fileURLs objectAtIndex:0] isFileURL])
3222         {
3223             NSURL* savefileURL = (NSURL *)[fileURLs objectAtIndex:0];
3224             /* The path property doesn't do the right thing except for
3225              * URLs with the file scheme. We had getFileSystemRepresentation
3226              * here before, but that wasn't introduced until OS X 10.9. */
3227             selectedSomething = [[savefileURL path] getCString:savefile 
3228                 maxLength:sizeof savefile encoding:NSMacOSRomanStringEncoding];
3229         }
3230     }
3231     
3232     if (selectedSomething)
3233     {
3234         /* Remember this so we can select it by default next time */
3235         record_current_savefile();
3236         
3237         /* Game is in progress */
3238         game_in_progress = TRUE;
3239         new_game = FALSE;
3240     }
3241     
3242     [pool drain];
3243 }
3244
3245 - (IBAction)saveGame:sender
3246 {
3247     /* Hack -- Forget messages */
3248     msg_flag = FALSE;
3249     
3250     /* Save the game */
3251     do_cmd_save_game(FALSE);
3252     
3253     /* Record the current save file so we can select it by default next time.
3254          * It's a little sketchy that this only happens when we save through the
3255          * menu; ideally game-triggered saves would trigger it too. */
3256     record_current_savefile();
3257 }
3258
3259 /**
3260  * Implement NSObject's validateMenuItem() method to override enabling or
3261  * disabling a menu item.  Note that, as of 10.14, validateMenuItem() is
3262  * deprecated in NSObject - it will be removed at some point and  the
3263  * application delegate will have to be declared as implementing the
3264  * NSMenuItemValidation protocol.
3265  */
3266 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
3267 {
3268     SEL sel = [menuItem action];
3269     NSInteger tag = [menuItem tag];
3270
3271     if( tag >= AngbandWindowMenuItemTagBase && tag < AngbandWindowMenuItemTagBase + ANGBAND_TERM_MAX )
3272     {
3273         if( tag == AngbandWindowMenuItemTagBase )
3274         {
3275             /* The main window should always be available and visible */
3276             return YES;
3277         }
3278         else
3279         {
3280             NSInteger subwindowNumber = tag - AngbandWindowMenuItemTagBase;
3281             return (window_flag[subwindowNumber] > 0);
3282         }
3283
3284         return NO;
3285     }
3286
3287     if (sel == @selector(newGame:))
3288     {
3289         return ! game_in_progress;
3290     }
3291     else if (sel == @selector(editFont:))
3292     {
3293         return YES;
3294     }
3295     else if (sel == @selector(openGame:))
3296     {
3297         return ! game_in_progress;
3298     }
3299     else if (sel == @selector(setRefreshRate:) && [superitem(menuItem) tag] == 150)
3300     {
3301         NSInteger fps = [[NSUserDefaults standardUserDefaults] integerForKey:AngbandFrameRateDefaultsKey];
3302         [menuItem setState: ([menuItem tag] == fps)];
3303         return YES;
3304     }
3305     else if( sel == @selector(setGraphicsMode:) )
3306     {
3307         NSInteger requestedGraphicsMode = [[NSUserDefaults standardUserDefaults] integerForKey:AngbandGraphicsDefaultsKey];
3308         [menuItem setState: (tag == requestedGraphicsMode)];
3309         return YES;
3310     }
3311     else if( sel == @selector(toggleSound:) )
3312     {
3313         BOOL is_on = [[NSUserDefaults standardUserDefaults]
3314                          boolForKey:AngbandSoundDefaultsKey];
3315
3316         [menuItem setState: ((is_on) ? NSOnState : NSOffState)];
3317         return YES;
3318     }
3319     else if( sel == @selector(sendAngbandCommand:) )
3320     {
3321         /* we only want to be able to send commands during an active game */
3322         return !!game_in_progress;
3323     }
3324     else return YES;
3325 }
3326
3327
3328 - (IBAction)setRefreshRate:(NSMenuItem *)menuItem
3329 {
3330     frames_per_second = [menuItem tag];
3331     [[NSUserDefaults angbandDefaults] setInteger:frames_per_second forKey:AngbandFrameRateDefaultsKey];
3332 }
3333
3334 - (IBAction)selectWindow: (id)sender
3335 {
3336     NSInteger subwindowNumber = [(NSMenuItem *)sender tag] - AngbandWindowMenuItemTagBase;
3337     AngbandContext *context = angband_term[subwindowNumber]->data;
3338     [context->primaryWindow makeKeyAndOrderFront: self];
3339         [context saveWindowVisibleToDefaults: YES];
3340 }
3341
3342 - (void)prepareWindowsMenu
3343 {
3344     /* Get the window menu with default items and add a separator and item for
3345          * the main window */
3346     NSMenu *windowsMenu = [[NSApplication sharedApplication] windowsMenu];
3347     [windowsMenu addItem: [NSMenuItem separatorItem]];
3348
3349     NSMenuItem *angbandItem = [[NSMenuItem alloc] initWithTitle: @"Hengband" action: @selector(selectWindow:) keyEquivalent: @"0"];
3350     [angbandItem setTarget: self];
3351     [angbandItem setTag: AngbandWindowMenuItemTagBase];
3352     [windowsMenu addItem: angbandItem];
3353     [angbandItem release];
3354
3355     /* Add items for the additional term windows */
3356     for( NSInteger i = 1; i < ANGBAND_TERM_MAX; i++ )
3357     {
3358         NSString *title = [NSString stringWithFormat: @"Term %ld", (long)i];
3359         NSString *keyEquivalent = [NSString stringWithFormat: @"%ld", (long)i];
3360         NSMenuItem *windowItem = [[NSMenuItem alloc] initWithTitle: title action: @selector(selectWindow:) keyEquivalent: keyEquivalent];
3361         [windowItem setTarget: self];
3362         [windowItem setTag: AngbandWindowMenuItemTagBase + i];
3363         [windowsMenu addItem: windowItem];
3364         [windowItem release];
3365     }
3366 }
3367
3368 - (IBAction)setGraphicsMode:(NSMenuItem *)sender
3369 {
3370     /* We stashed the graphics mode ID in the menu item's tag */
3371     graf_mode_req = [sender tag];
3372
3373     /* Stash it in UserDefaults */
3374     [[NSUserDefaults angbandDefaults] setInteger:graf_mode_req forKey:AngbandGraphicsDefaultsKey];
3375     [[NSUserDefaults angbandDefaults] synchronize];
3376     
3377     if (game_in_progress)
3378     {
3379         /* Hack -- Force redraw */
3380         do_cmd_redraw();
3381         
3382         /* Wake up the event loop so it notices the change */
3383         wakeup_event_loop();
3384     }
3385 }
3386
3387 - (IBAction) toggleSound: (NSMenuItem *) sender
3388 {
3389     BOOL is_on = (sender.state == NSOnState);
3390
3391     /* Toggle the state and update the Angband global and preferences. */
3392     sender.state = (is_on) ? NSOffState : NSOnState;
3393     use_sound = (is_on) ? FALSE : TRUE;
3394     [[NSUserDefaults angbandDefaults] setBool:(! is_on)
3395                                       forKey:AngbandSoundDefaultsKey];
3396 }
3397
3398 /**
3399  *  Send a command to Angband via a menu item. This places the appropriate key
3400  * down events into the queue so that it seems like the user pressed them
3401  * (instead of trying to use the term directly).
3402  */
3403 - (void)sendAngbandCommand: (id)sender
3404 {
3405     NSMenuItem *menuItem = (NSMenuItem *)sender;
3406     NSString *command = [self.commandMenuTagMap objectForKey: [NSNumber numberWithInteger: [menuItem tag]]];
3407     NSInteger windowNumber = [((AngbandContext *)angband_term[0]->data)->primaryWindow windowNumber];
3408
3409     /* Send a \ to bypass keymaps */
3410     NSEvent *escape = [NSEvent keyEventWithType: NSKeyDown
3411                                        location: NSZeroPoint
3412                                   modifierFlags: 0
3413                                       timestamp: 0.0
3414                                    windowNumber: windowNumber
3415                                         context: nil
3416                                      characters: @"\\"
3417                     charactersIgnoringModifiers: @"\\"
3418                                       isARepeat: NO
3419                                         keyCode: 0];
3420     [[NSApplication sharedApplication] postEvent: escape atStart: NO];
3421
3422     /* Send the actual command (from the original command set) */
3423     NSEvent *keyDown = [NSEvent keyEventWithType: NSKeyDown
3424                                         location: NSZeroPoint
3425                                    modifierFlags: 0
3426                                        timestamp: 0.0
3427                                     windowNumber: windowNumber
3428                                          context: nil
3429                                       characters: command
3430                      charactersIgnoringModifiers: command
3431                                        isARepeat: NO
3432                                          keyCode: 0];
3433     [[NSApplication sharedApplication] postEvent: keyDown atStart: NO];
3434 }
3435
3436 /**
3437  *  Set up the command menu dynamically, based on CommandMenu.plist.
3438  */
3439 - (void)prepareCommandMenu
3440 {
3441     NSString *commandMenuPath = [[NSBundle mainBundle] pathForResource: @"CommandMenu" ofType: @"plist"];
3442     NSArray *commandMenuItems = [[NSArray alloc] initWithContentsOfFile: commandMenuPath];
3443     NSMutableDictionary *angbandCommands = [[NSMutableDictionary alloc] init];
3444     NSString *tblname = @"CommandMenu";
3445     NSInteger tagOffset = 0;
3446
3447     for( NSDictionary *item in commandMenuItems )
3448     {
3449         BOOL useShiftModifier = [[item valueForKey: @"ShiftModifier"] boolValue];
3450         BOOL useOptionModifier = [[item valueForKey: @"OptionModifier"] boolValue];
3451         NSUInteger keyModifiers = NSCommandKeyMask;
3452         keyModifiers |= (useShiftModifier) ? NSShiftKeyMask : 0;
3453         keyModifiers |= (useOptionModifier) ? NSAlternateKeyMask : 0;
3454
3455         NSString *lookup = [item valueForKey: @"Title"];
3456         NSString *title = NSLocalizedStringWithDefaultValue(
3457             lookup, tblname, [NSBundle mainBundle], lookup, @"");
3458         NSString *key = [item valueForKey: @"KeyEquivalent"];
3459         NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle: title action: @selector(sendAngbandCommand:) keyEquivalent: key];
3460         [menuItem setTarget: self];
3461         [menuItem setKeyEquivalentModifierMask: keyModifiers];
3462         [menuItem setTag: AngbandCommandMenuItemTagBase + tagOffset];
3463         [self.commandMenu addItem: menuItem];
3464         [menuItem release];
3465
3466         NSString *angbandCommand = [item valueForKey: @"AngbandCommand"];
3467         [angbandCommands setObject: angbandCommand forKey: [NSNumber numberWithInteger: [menuItem tag]]];
3468         tagOffset++;
3469     }
3470
3471     [commandMenuItems release];
3472
3473     NSDictionary *safeCommands = [[NSDictionary alloc] initWithDictionary: angbandCommands];
3474     self.commandMenuTagMap = safeCommands;
3475     [safeCommands release];
3476     [angbandCommands release];
3477 }
3478
3479 - (void)awakeFromNib
3480 {
3481     [super awakeFromNib];
3482
3483     [self prepareWindowsMenu];
3484     [self prepareCommandMenu];
3485 }
3486
3487 - (void)applicationDidFinishLaunching:sender
3488 {
3489     [AngbandContext beginGame];
3490     
3491     /* Once beginGame finished, the game is over - that's how Angband works,
3492          * and we should quit */
3493     game_is_finished = TRUE;
3494     [NSApp terminate:self];
3495 }
3496
3497 - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
3498 {
3499     if (p_ptr->playing == FALSE || game_is_finished == TRUE)
3500     {
3501         return NSTerminateNow;
3502     }
3503     else if (! inkey_flag)
3504     {
3505         /* For compatibility with other ports, do not quit in this case */
3506         return NSTerminateCancel;
3507     }
3508     else
3509     {
3510         /* Stop playing */
3511         /* player->upkeep->playing = FALSE; */
3512
3513         /* Post an escape event so that we can return from our get-key-event
3514                  * function */
3515         wakeup_event_loop();
3516         quit_when_ready = true;
3517         /* Must return Cancel, not Later, because we need to get out of the
3518                  * run loop and back to Angband's loop */
3519         return NSTerminateCancel;
3520     }
3521 }
3522
3523 /**
3524  * Dynamically build the Graphics menu
3525  */
3526 - (void)menuNeedsUpdate:(NSMenu *)menu {
3527     
3528     /* Only the graphics menu is dynamic */
3529     if (! [menu isEqual:self.graphicsMenu])
3530         return;
3531     
3532     /* If it's non-empty, then we've already built it. Currently graphics modes
3533          * won't change once created; if they ever can we can remove this check.
3534      * Note that the check mark does change, but that's handled in
3535          * validateMenuItem: instead of menuNeedsUpdate: */
3536     if ([menu numberOfItems] > 0)
3537         return;
3538     
3539     /* This is the action for all these menu items */
3540     SEL action = @selector(setGraphicsMode:);
3541     
3542     /* Add an initial Classic ASCII menu item */
3543     NSString *tblname = @"GraphicsMenu";
3544     NSString *key = @"Classic ASCII";
3545     NSString *title = NSLocalizedStringWithDefaultValue(
3546         key, tblname, [NSBundle mainBundle], key, @"");
3547     NSMenuItem *classicItem = [menu addItemWithTitle:title action:action keyEquivalent:@""];
3548     [classicItem setTag:GRAPHICS_NONE];
3549     
3550     /* Walk through the list of graphics modes */
3551     if (graphics_modes) {
3552         NSInteger i;
3553
3554         for (i=0; graphics_modes[i].pNext; i++)
3555         {
3556             const graphics_mode *graf = &graphics_modes[i];
3557
3558             if (graf->grafID == GRAPHICS_NONE) {
3559                 continue;
3560             }
3561             /* Make the title. NSMenuItem throws on a nil title, so ensure it's
3562                    * not nil. */
3563             key = [[NSString alloc] initWithUTF8String:graf->menuname];
3564             title = NSLocalizedStringWithDefaultValue(
3565                 key, tblname, [NSBundle mainBundle], key, @"");
3566         
3567             /* Make the item */
3568             NSMenuItem *item = [menu addItemWithTitle:title action:action keyEquivalent:@""];
3569             [key release];
3570             [item setTag:graf->grafID];
3571         }
3572     }
3573 }
3574
3575 /**
3576  * Delegate method that gets called if we're asked to open a file.
3577  */
3578 - (BOOL)application:(NSApplication *)sender openFiles:(NSArray *)filenames
3579 {
3580     /* Can't open a file once we've started */
3581     if (game_in_progress) return NO;
3582     
3583     /* We can only open one file. Use the last one. */
3584     NSString *file = [filenames lastObject];
3585     if (! file) return NO;
3586     
3587     /* Put it in savefile */
3588     if (! [file getFileSystemRepresentation:savefile maxLength:sizeof savefile])
3589                 return NO;
3590     
3591     game_in_progress = TRUE;
3592     new_game = FALSE;
3593
3594     /* Wake us up in case this arrives while we're sitting at the Welcome
3595          * screen! */
3596     wakeup_event_loop();
3597     
3598     return YES;
3599 }
3600
3601 @end
3602
3603 int main(int argc, char* argv[])
3604 {
3605     NSApplicationMain(argc, (void*)argv);    
3606     return (0);
3607 }
3608
3609 #endif /* MACINTOSH || MACH_O_COCOA */