OSDN Git Service

To make further changes easier (to better match with current Apple documentation...
[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/AppDelegate.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 NSString * const AngbandDirectoryNameLib = @"lib";
36 static NSString * const AngbandDirectoryNameBase = @"Hengband";
37
38 static NSString * const AngbandMessageCatalog = @"Localizable";
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 AngbandBigTileDefaultsKey = @"UseBigTiles";
45 static NSString * const AngbandFrameRateDefaultsKey = @"FramesPerSecond";
46 static NSString * const AngbandSoundDefaultsKey = @"AllowSound";
47 static NSInteger const AngbandWindowMenuItemTagBase = 1000;
48 static NSInteger const AngbandCommandMenuItemTagBase = 2000;
49
50 /* We can blit to a large layer or image and then scale it down during live
51  * resize, which makes resizing much faster, at the cost of some image quality
52  * during resizing */
53 #ifndef USE_LIVE_RESIZE_CACHE
54 # define USE_LIVE_RESIZE_CACHE 1
55 #endif
56
57 /* Global defines etc from Angband 3.5-dev - NRM */
58 #define ANGBAND_TERM_MAX 8
59
60 static bool new_game = TRUE;
61
62 #define MAX_COLORS 256
63 #define MSG_MAX SOUND_MAX
64
65 /* End Angband stuff - NRM */
66
67 /* Application defined event numbers */
68 enum
69 {
70     AngbandEventWakeup = 1
71 };
72
73 /* Redeclare some 10.7 constants and methods so we can build on 10.6 */
74 enum
75 {
76     Angband_NSWindowCollectionBehaviorFullScreenPrimary = 1 << 7,
77     Angband_NSWindowCollectionBehaviorFullScreenAuxiliary = 1 << 8
78 };
79
80 @interface NSWindow (AngbandLionRedeclares)
81 - (void)setRestorable:(BOOL)flag;
82 @end
83
84 /* Delay handling of pre-emptive "quit" event */
85 static BOOL quit_when_ready = FALSE;
86
87 /* Set to indicate the game is over and we can quit without delay */
88 static Boolean game_is_finished = FALSE;
89
90 /* Our frames per second (e.g. 60). A value of 0 means unthrottled. */
91 static int frames_per_second;
92
93 @class AngbandView;
94
95 /**
96  * Load sound effects based on sound.cfg within the xtra/sound directory;
97  * bridge to Cocoa to use NSSound for simple loading and playback, avoiding
98  * I/O latency by caching all sounds at the start.  Inherits full sound
99  * format support from Quicktime base/plugins.
100  * pelpel favoured a plist-based parser for the future but .cfg support
101  * improves cross-platform compatibility.
102  */
103 @interface AngbandSoundCatalog : NSObject {
104 @private
105     /**
106      * Stores instances of NSSound keyed by path so the same sound can be
107      * used for multiple events.
108      */
109     NSMutableDictionary *soundsByPath;
110     /**
111      * Stores arrays of NSSound keyed by event number.
112      */
113     NSMutableDictionary *soundArraysByEvent;
114 }
115
116 /**
117  * If NO, then playSound effectively becomes a do nothing operation.
118  */
119 @property (getter=isEnabled) BOOL enabled;
120
121 /**
122  * Set up for lazy initialization in playSound().  Set enabled to NO.
123  */
124 - (id)init;
125
126 /**
127  * Releases the resources acquired for the catalog.
128  */
129 - (void)dispose;
130
131 /**
132  * If self.enabled is YES and the given event has one or more sounds
133  * corresponding to it in the catalog, plays one of those sounds, chosen at
134  * random.
135  */
136 - (void)playSound:(int)event;
137
138 /**
139  * Impose an arbitary limit on number of possible samples per event.
140  * Currently not declaring this as a class property for compatibility with
141  * versions of Xcode prior to 8.
142  */
143 + (int)maxSamples;
144
145 /**
146  * Return the shared sound catalog instance, creating it if it does not
147  * exist yet.  Currently not declaring this as a class property for
148  * compatibility with versions of Xcode prior to 8.
149  */
150 + (AngbandSoundCatalog*)sharedSounds;
151
152 /**
153  * Release any resouces associated with shared sounds.
154  */
155 + (void)clearSharedSounds;
156
157 @end
158
159 /**
160  * This is a helper function for AngbandSoundCatalog's sharedSounds and
161  * clearSharedSounds.  Implement a Singleton pattern.
162  */
163 static __strong AngbandSoundCatalog** get_shared_sounds(void)
164 {
165     static __strong AngbandSoundCatalog* catalog = nil;
166
167     return &catalog;
168 }
169
170 @implementation AngbandSoundCatalog
171
172 - (id)init {
173     if (self = [super init]) {
174         self->soundsByPath = nil;
175         self->soundArraysByEvent = nil;
176         self->_enabled = NO;
177     }
178     return self;
179 }
180
181 - (void)dispose {
182     self->soundsByPath = nil;
183     self->soundArraysByEvent = nil;
184 }
185
186 - (void)playSound:(int)event {
187     if (! self.enabled) {
188         return;
189     }
190
191     /* Initialize when the first sound is played. */
192     if (self->soundArraysByEvent == nil) {
193         /* Build the "sound" path */
194         char sound_dir[1024];
195         path_build(sound_dir, sizeof(sound_dir), ANGBAND_DIR_XTRA, "sound");
196
197         /* Find and open the config file */
198         char path[1024];
199         path_build(path, sizeof(path), sound_dir, "sound.cfg");
200         FILE *fff = my_fopen(path, "r");
201
202         /* Handle errors */
203         if (!fff) {
204             NSLog(@"The sound configuration file could not be opened.");
205             return;
206         }
207
208         self->soundsByPath = [[NSMutableDictionary alloc] init];
209         self->soundArraysByEvent = [[NSMutableDictionary alloc] init];
210         @autoreleasepool {
211             /*
212              * This loop may take a while depending on the count and size of
213              * samples to load.
214              */
215
216             /* Parse the file */
217             /* Lines are always of the form "name = sample [sample ...]" */
218             char buffer[2048];
219             while (my_fgets(fff, buffer, sizeof(buffer)) == 0) {
220                 char *msg_name;
221                 char *cfg_sample_list;
222                 char *search;
223                 char *cur_token;
224                 char *next_token;
225                 int event;
226
227                 /* Skip anything not beginning with an alphabetic character */
228                 if (!buffer[0] || !isalpha((unsigned char)buffer[0])) continue;
229
230                 /* Split the line into two: message name, and the rest */
231                 search = strchr(buffer, ' ');
232                 cfg_sample_list = strchr(search + 1, ' ');
233                 if (!search) continue;
234                 if (!cfg_sample_list) continue;
235
236                 /* Set the message name, and terminate at first space */
237                 msg_name = buffer;
238                 search[0] = '\0';
239
240                 /* Make sure this is a valid event name */
241                 for (event = MSG_MAX - 1; event >= 0; event--) {
242                     if (strcmp(msg_name, angband_sound_name[event]) == 0)
243                         break;
244                 }
245                 if (event < 0) continue;
246
247                 /*
248                  * Advance the sample list pointer so it's at the beginning of
249                  * text.
250                  */
251                 cfg_sample_list++;
252                 if (!cfg_sample_list[0]) continue;
253
254                 /* Terminate the current token */
255                 cur_token = cfg_sample_list;
256                 search = strchr(cur_token, ' ');
257                 if (search) {
258                     search[0] = '\0';
259                     next_token = search + 1;
260                 } else {
261                     next_token = NULL;
262                 }
263
264                 /*
265                  * Now we find all the sample names and add them one by one
266                  */
267                 while (cur_token) {
268                     NSMutableArray *soundSamples =
269                         [self->soundArraysByEvent
270                              objectForKey:[NSNumber numberWithInteger:event]];
271                     if (soundSamples == nil) {
272                         soundSamples = [[NSMutableArray alloc] init];
273                         [self->soundArraysByEvent
274                              setObject:soundSamples
275                              forKey:[NSNumber numberWithInteger:event]];
276                     }
277                     int num = (int) soundSamples.count;
278
279                     /* Don't allow too many samples */
280                     if (num >= [AngbandSoundCatalog maxSamples]) break;
281
282                     NSString *token_string =
283                         [NSString stringWithUTF8String:cur_token];
284                     NSSound *sound =
285                         [self->soundsByPath objectForKey:token_string];
286
287                     if (! sound) {
288                         /*
289                          * We have to load the sound. Build the path to the
290                          * sample.
291                          */
292                         path_build(path, sizeof(path), sound_dir, cur_token);
293                         struct stat stb;
294                         if (stat(path, &stb) == 0) {
295                             /* Load the sound into memory */
296                             sound = [[NSSound alloc]
297                                          initWithContentsOfFile:[NSString stringWithUTF8String:path]
298                                          byReference:YES];
299                             if (sound) {
300                                 [self->soundsByPath setObject:sound
301                                             forKey:token_string];
302                             }
303                         }
304                     }
305
306                     /* Store it if we loaded it */
307                     if (sound) {
308                         [soundSamples addObject:sound];
309                     }
310
311                     /* Figure out next token */
312                     cur_token = next_token;
313                     if (next_token) {
314                          /* Try to find a space */
315                          search = strchr(cur_token, ' ');
316
317                          /*
318                           * If we can find one, terminate, and set new "next".
319                           */
320                          if (search) {
321                              search[0] = '\0';
322                              next_token = search + 1;
323                          } else {
324                              /* Otherwise prevent infinite looping */
325                              next_token = NULL;
326                          }
327                     }
328                 }
329             }
330         }
331
332         /* Close the file */
333         my_fclose(fff);
334     }
335
336     @autoreleasepool {
337         NSMutableArray *samples =
338             [self->soundArraysByEvent
339                  objectForKey:[NSNumber numberWithInteger:event]];
340
341         if (samples == nil || samples.count == 0) {
342             return;
343         }
344
345         /* Choose a random event. */
346         int s = randint0((int) samples.count);
347         NSSound *sound = samples[s];
348
349         if ([sound isPlaying])
350             [sound stop];
351
352         /* Play the sound. */
353         [sound play];
354     }
355 }
356
357 + (int)maxSamples {
358     return 16;
359 }
360
361 /**
362  * For sharedSounds and clearSharedSounds.
363  */
364 static __strong AngbandSoundCatalog* gSharedSounds = nil;
365
366 + (AngbandSoundCatalog*)sharedSounds {
367     if (gSharedSounds == nil) {
368         gSharedSounds = [[AngbandSoundCatalog alloc] init];
369     }
370     return gSharedSounds;;
371 }
372
373 + (void)clearSharedSounds {
374     gSharedSounds = nil;
375 }
376
377 @end
378
379 /*
380  * To handle fonts where an individual glyph's bounding box can extend into
381  * neighboring columns, Term_curs_cocoa(), Term_pict_cocoa(),
382  * Term_text_cocoa(), and Term_wipe_cocoa() merely record what needs to be
383  * done with the actual drawing happening in response to the notification to
384  * flush all rows, the TERM_XTRA_FRESH case in Term_xtra_cocoa().  Can not use
385  * the TERM_XTRA_FROSH notification (the per-row flush), since with a software
386  * cursor, there are calls to Term_pict_cocoa(), Term_text_cocoa(), or
387  * Term_wipe_cocoa() to take care of the old cursor position which are not
388  * followed by a row flush.
389  */
390 enum PendingCellChangeType {
391     CELL_CHANGE_NONE = 0,
392     CELL_CHANGE_WIPE,
393     CELL_CHANGE_TEXT,
394     CELL_CHANGE_TILE
395 };
396 struct PendingTextChange {
397     wchar_t glyph;
398     int color;
399     /*
400      * Is YES if glyph is a character that takes up two columns (i.e.
401      * Japanese kanji); otherwise it is NO.
402      */
403     BOOL doubleWidth;
404 };
405 struct PendingTileChange {
406     char fgdCol, fgdRow, bckCol, bckRow;
407 };
408 struct PendingCellChange {
409     union { struct PendingTextChange txc; struct PendingTileChange tic; } v;
410     enum PendingCellChangeType changeType;
411 };
412
413 @interface PendingTermChanges : NSObject {
414 @private
415     int *colBounds;
416     struct PendingCellChange **changesByRow;
417 }
418
419 /**
420  * Returns YES if nCol and nRow are a feasible size for the pending changes.
421  * Otherwise, returns NO.
422  */
423 + (BOOL)isValidSize:(int)nCol rows:(int)nRow;
424
425 /**
426  * Initialize with zero columns and zero rows.
427  */
428 - (id)init;
429
430 /**
431  * Initialize with nCol columns and nRow rows.  No changes will be marked.
432  */
433 - (id)initWithColumnsRows:(int)nCol rows:(int)nRow NS_DESIGNATED_INITIALIZER;
434
435 /**
436  * Clears all marked changes.
437  */
438 - (void)clear;
439
440 /**
441  * Changes the bounds over which changes are recorded.  Has the side effect
442  * of clearing any marked changes.  Will throw an exception if nCol or nRow
443  * is negative.
444  */
445 - (void)resize:(int)nCol rows:(int)nRow;
446
447 /**
448  * Mark the cell, (iCol, iRow), as having changed text.
449  */
450 - (void)markTextChange:(int)iCol row:(int)iRow glyph:(wchar_t)g color:(int)c
451          isDoubleWidth:(BOOL)dw;
452
453 /**
454  * Mark the cell, (iCol, iRow), as having a changed tile.
455  */
456 - (void)markTileChange:(int)iCol row:(int)iRow
457          foregroundCol:(char)fc foregroundRow:(char)fr
458          backgroundCol:(char)bc backgroundRow:(char)br;
459
460 /**
461  * Mark the cells from (iCol, iRow) to (iCol + nCol - 1, iRow) as wiped.
462  */
463 - (void)markWipeRange:(int)iCol row:(int)iRow n:(int)nCol;
464
465 /**
466  * Mark the location of the cursor.  The cursor will be the standard size:
467  * one cell.
468  */
469 - (void)markCursor:(int)iCol row:(int)iRow;
470
471 /**
472  * Mark the location of the cursor.  The cursor will be w cells wide and
473  * h cells tall and the given location is the position of the upper left
474  * corner.
475  */
476 - (void)markBigCursor:(int)iCol row:(int)iRow
477             cellsWide:(int)w cellsHigh:(int)h;
478
479 /**
480  * Return the zero-based index of the first column changed for the given
481  * zero-based row index.  If there are no changes in the row, the returned
482  * value will be the number of columns.
483  */
484 - (int)getFirstChangedColumnInRow:(int)iRow;
485
486 /**
487  * Return the zero-based index of the last column changed for the given
488  * zero-based row index.  If there are no changes in the row, the returned
489  * value will be -1.
490  */
491 - (int)getLastChangedColumnInRow:(int)iRow;
492
493 /**
494  * Return the type of change at the given cell, (iCol, iRow).
495  */
496 - (enum PendingCellChangeType)getCellChangeType:(int)iCol row:(int)iRow;
497
498 /**
499  * Return the nature of a text change at the given cell, (iCol, iRow).
500  * Will throw an exception if [obj getCellChangeType:iCol row:iRow] is
501  * neither CELL_CHANGE_TEXT nor CELL_CHANGE_WIPE.
502  */
503 - (struct PendingTextChange)getCellTextChange:(int)iCol row:(int)iRow;
504
505 /**
506  * Return the nature of a tile change at the given cell, (iCol, iRow).
507  * Will throw an exception if [obj getCellChangeType:iCol row:iRow] is
508  * different than CELL_CHANGE_TILE.
509  */
510 - (struct PendingTileChange)getCellTileChange:(int)iCol row:(int)iRow;
511
512 /**
513  * Is the number of columns for recording changes.
514  */
515 @property (readonly) int columnCount;
516
517 /**
518  * Is the number of rows for recording changes.
519  */
520 @property (readonly) int rowCount;
521
522 /**
523  * Will be YES if there are any pending changes to locations rendered as text.
524  * Otherwise, it will be NO.
525  */
526 @property (readonly) BOOL hasTextChanges;
527
528 /**
529  * Will be YES if there are any pending changes to locations rendered as tiles.
530  * Otherwise, it will be NO.
531  */
532 @property (readonly) BOOL hasTileChanges;
533
534 /**
535  * Will be YES if there are any pending wipes.  Otherwise, it will be NO.
536  */
537 @property (readonly) BOOL hasWipeChanges;
538
539 /**
540  * Is the zero-based index of the first row with changes.  Will be equal to
541  * the number of rows if there are no changes.
542  */
543 @property (readonly) int firstChangedRow;
544
545 /**
546  * Is the zero-based index of the last row with changes.  Will be equal to
547  * -1 if there are no changes.
548  */
549 @property (readonly) int lastChangedRow;
550
551 /**
552  * Is the zero-based index for the column with the upper left corner of the
553  * cursor.  It will be -1 if the cursor position has not been set since the
554  * changes were cleared.
555  */
556 @property (readonly) int cursorColumn;
557
558 /**
559  * Is the zero-based index for the row with the upper left corner of the
560  * cursor.  It will be -1 if the cursor position has not been set since the
561  * changes were cleared.
562  */
563 @property (readonly) int cursorRow;
564
565 /**
566  * Is the cursor width in number of cells.
567  */
568 @property (readonly) int cursorWidth;
569
570 /**
571  * Is the cursor height in number of cells.
572  */
573 @property (readonly) int cursorHeight;
574
575 /**
576  * This is a helper for the mark* messages.
577  */
578 - (void)setupForChange:(int)iCol row:(int)iRow n:(int)nCol;
579
580 /**
581  * Throw an exception if the given range of column indices is invalid
582  * (including non-positive values for nCol).
583  */
584 - (void)checkColumnIndices:(int)iCol n:(int)nCol;
585
586 /**
587  * Throw an exception if the given row index is invalid.
588  */
589 - (void)checkRowIndex:(int)iRow;
590
591 @end
592
593 @implementation PendingTermChanges
594
595 + (BOOL)isValidSize:(int)nCol rows:(int)nRow
596 {
597     if (nCol < 0 ||
598         (size_t) nCol > SIZE_MAX / sizeof(struct PendingCellChange) ||
599         nRow < 0 ||
600         (size_t) nRow > SIZE_MAX / sizeof(struct PendingCellChange*) ||
601         (size_t) nRow > SIZE_MAX / (2 * sizeof(int))) {
602         return NO;
603     }
604     return YES;
605 }
606
607 - (id)init
608 {
609     return [self initWithColumnsRows:0 rows:0];
610 }
611
612 - (id)initWithColumnsRows:(int)nCol rows:(int)nRow
613 {
614     if (self = [super init]) {
615         if (! [PendingTermChanges isValidSize:nCol rows:nRow]) {
616             return nil;
617         }
618         self->colBounds = malloc((size_t) 2 * sizeof(int) * nRow);
619         if (self->colBounds == 0 && nRow > 0) {
620             return nil;
621         }
622         self->changesByRow = calloc(nRow, sizeof(struct PendingCellChange*));
623         if (self->changesByRow == 0 && nRow > 0) {
624             free(self->colBounds);
625             return nil;
626         }
627         for (int i = 0; i < nRow + nRow; i += 2) {
628             self->colBounds[i] = nCol;
629             self->colBounds[i + 1] = -1;
630         }
631         self->_columnCount = nCol;
632         self->_rowCount = nRow;
633         self->_hasTextChanges = NO;
634         self->_hasTileChanges = NO;
635         self->_hasWipeChanges = NO;
636         self->_firstChangedRow = nRow;
637         self->_lastChangedRow = -1;
638         self->_cursorColumn = -1;
639         self->_cursorRow = -1;
640         self->_cursorWidth = 1;
641         self->_cursorHeight = 1;
642     }
643     return self;
644 }
645
646 - (void)dealloc
647 {
648     if (self->changesByRow != 0) {
649         for (int i = 0; i < self.rowCount; ++i) {
650             if (self->changesByRow[i] != 0) {
651                 free(self->changesByRow[i]);
652                 self->changesByRow[i] = 0;
653             }
654         }
655         free(self->changesByRow);
656         self->changesByRow = 0;
657     }
658     if (self->colBounds != 0) {
659         free(self->colBounds);
660         self->colBounds = 0;
661     }
662 }
663
664 - (void)clear
665 {
666     for (int i = 0; i < self.rowCount; ++i) {
667         self->colBounds[i + i] = self.columnCount;
668         self->colBounds[i + i + 1] = -1;
669         if (self->changesByRow[i] != 0) {
670             free(self->changesByRow[i]);
671             self->changesByRow[i] = 0;
672         }
673     }
674     self->_hasTextChanges = NO;
675     self->_hasTileChanges = NO;
676     self->_hasWipeChanges = NO;
677     self->_firstChangedRow = self.rowCount;
678     self->_lastChangedRow = -1;
679     self->_cursorColumn = -1;
680     self->_cursorRow = -1;
681     self->_cursorWidth = 1;
682     self->_cursorHeight = 1;
683 }
684
685 - (void)resize:(int)nCol rows:(int)nRow
686 {
687     if (! [PendingTermChanges isValidSize:nCol rows:nRow]) {
688         NSException *exc = [NSException
689                                exceptionWithName:@"PendingTermChangesRowsColumns"
690                                reason:@"resize called with number of columns or rows that is negative or too large"
691                                userInfo:nil];
692         @throw exc;
693     }
694
695     int *cb = malloc((size_t) 2 * sizeof(int) * nRow);
696     struct PendingCellChange** cbr =
697         calloc(nRow, sizeof(struct PendingCellChange*));
698     int i;
699
700     if ((cb == 0 || cbr == 0) && nRow > 0) {
701         if (cbr != 0) {
702             free(cbr);
703         }
704         if (cb != 0) {
705             free(cb);
706         }
707
708         NSException *exc = [NSException
709                                exceptionWithName:@"OutOfMemory"
710                                reason:@"resize called for PendingTermChanges"
711                                userInfo:nil];
712         @throw exc;
713     }
714
715     for (i = 0; i < nRow; ++i) {
716         cb[i + i] = nCol;
717         cb[i + i + 1] = -1;
718     }
719     if (self->changesByRow != 0) {
720         for (i = 0; i < self.rowCount; ++i) {
721             if (self->changesByRow[i] != 0) {
722                 free(self->changesByRow[i]);
723                 self->changesByRow[i] = 0;
724             }
725         }
726         free(self->changesByRow);
727     }
728     if (self->colBounds != 0) {
729         free(self->colBounds);
730     }
731
732     self->colBounds = cb;
733     self->changesByRow = cbr;
734     self->_columnCount = nCol;
735     self->_rowCount = nRow;
736     self->_hasTextChanges = NO;
737     self->_hasTileChanges = NO;
738     self->_hasWipeChanges = NO;
739     self->_firstChangedRow = self.rowCount;
740     self->_lastChangedRow = -1;
741     self->_cursorColumn = -1;
742     self->_cursorRow = -1;
743     self->_cursorWidth = 1;
744     self->_cursorHeight = 1;
745 }
746
747 - (void)markTextChange:(int)iCol row:(int)iRow glyph:(wchar_t)g color:(int)c
748          isDoubleWidth:(BOOL)dw
749 {
750     [self setupForChange:iCol row:iRow n:((dw) ? 2 : 1)];
751     struct PendingCellChange *pcc = self->changesByRow[iRow] + iCol;
752     pcc->v.txc.glyph = g;
753     pcc->v.txc.color = c;
754     pcc->v.txc.doubleWidth = dw;
755     pcc->changeType = CELL_CHANGE_TEXT;
756     /*
757      * Fill in a dummy since the previous character will take up two columns.
758      */
759     if (dw) {
760         pcc[1].v.txc.glyph = 0;
761         pcc[1].v.txc.color = c;
762         pcc[1].v.txc.doubleWidth = NO;
763         pcc[1].changeType = CELL_CHANGE_TEXT;
764     }
765     self->_hasTextChanges = YES;
766 }
767
768 - (void)markTileChange:(int)iCol row:(int)iRow
769          foregroundCol:(char)fc foregroundRow:(char)fr
770          backgroundCol:(char)bc backgroundRow:(char)br
771 {
772     [self setupForChange:iCol row:iRow n:1];
773     struct PendingCellChange *pcc = self->changesByRow[iRow] + iCol;
774     pcc->v.tic.fgdCol = fc;
775     pcc->v.tic.fgdRow = fr;
776     pcc->v.tic.bckCol = bc;
777     pcc->v.tic.bckRow = br;
778     pcc->changeType = CELL_CHANGE_TILE;
779     self->_hasTileChanges = YES;
780 }
781
782 - (void)markWipeRange:(int)iCol row:(int)iRow n:(int)nCol
783 {
784     [self setupForChange:iCol row:iRow n:nCol];
785     struct PendingCellChange *pcc = self->changesByRow[iRow] + iCol;
786     for (int i = 0; i < nCol; ++i) {
787         pcc[i].v.txc.glyph = 0;
788         pcc[i].v.txc.color = 0;
789         pcc[i].changeType = CELL_CHANGE_WIPE;
790     }
791     self->_hasWipeChanges = YES;
792 }
793
794 - (void)markCursor:(int)iCol row:(int)iRow
795 {
796     /* Allow negative indices to indicate an invalid cursor. */
797     [self checkColumnIndices:((iCol >= 0) ? iCol : 0) n:1];
798     [self checkRowIndex:((iRow >= 0) ? iRow : 0)];
799     self->_cursorColumn = iCol;
800     self->_cursorRow = iRow;
801     self->_cursorWidth = 1;
802     self->_cursorHeight = 1;
803 }
804
805 - (void)markBigCursor:(int)iCol row:(int)iRow
806             cellsWide:(int)w cellsHigh:(int)h
807 {
808     /* Allow negative indices to indicate an invalid cursor. */
809     [self checkColumnIndices:((iCol >= 0) ? iCol : 0) n:1];
810     [self checkRowIndex:((iRow >= 0) ? iRow : 0)];
811     if (w < 1 || h < 1) {
812         NSException *exc = [NSException
813                                exceptionWithName:@"InvalidCursorDimensions"
814                                reason:@"markBigCursor called for PendingTermChanges"
815                                userInfo:nil];
816         @throw exc;
817     }
818     self->_cursorColumn = iCol;
819     self->_cursorRow = iRow;
820     self->_cursorWidth = w;
821     self->_cursorHeight = h;
822 }
823
824 - (void)setupForChange:(int)iCol row:(int)iRow n:(int)nCol
825 {
826     [self checkColumnIndices:iCol n:nCol];
827     [self checkRowIndex:iRow];
828     if (self->changesByRow[iRow] == 0) {
829         self->changesByRow[iRow] =
830             malloc(self.columnCount * sizeof(struct PendingCellChange));
831         if (self->changesByRow[iRow] == 0 && self.columnCount > 0) {
832             NSException *exc = [NSException
833                                    exceptionWithName:@"OutOfMemory"
834                                    reason:@"setupForChange called for PendingTermChanges"
835                                    userInfo:nil];
836             @throw exc;
837         }
838         struct PendingCellChange* pcc = self->changesByRow[iRow];
839         for (int i = 0; i < self.columnCount; ++i) {
840             pcc[i].changeType = CELL_CHANGE_NONE;
841         }
842     }
843     if (self.firstChangedRow > iRow) {
844         self->_firstChangedRow = iRow;
845     }
846     if (self.lastChangedRow < iRow) {
847         self->_lastChangedRow = iRow;
848     }
849     if ([self getFirstChangedColumnInRow:iRow] > iCol) {
850         self->colBounds[iRow + iRow] = iCol;
851     }
852     if ([self getLastChangedColumnInRow:iRow] < iCol + nCol - 1) {
853         self->colBounds[iRow + iRow + 1] = iCol + nCol - 1;
854     }
855 }
856
857 - (int)getFirstChangedColumnInRow:(int)iRow
858 {
859     [self checkRowIndex:iRow];
860     return self->colBounds[iRow + iRow];
861 }
862
863 - (int)getLastChangedColumnInRow:(int)iRow
864 {
865     [self checkRowIndex:iRow];
866     return self->colBounds[iRow + iRow + 1];
867 }
868
869 - (enum PendingCellChangeType)getCellChangeType:(int)iCol row:(int)iRow
870 {
871     [self checkColumnIndices:iCol n:1];
872     [self checkRowIndex:iRow];
873     if (iRow < self.firstChangedRow || iRow > self.lastChangedRow) {
874         return CELL_CHANGE_NONE;
875     }
876     if (iCol < [self getFirstChangedColumnInRow:iRow] ||
877         iCol > [self getLastChangedColumnInRow:iRow]) {
878         return CELL_CHANGE_NONE;
879     }
880     return self->changesByRow[iRow][iCol].changeType;
881 }
882
883 - (struct PendingTextChange)getCellTextChange:(int)iCol row:(int)iRow
884 {
885     [self checkColumnIndices:iCol n:1];
886     [self checkRowIndex:iRow];
887     if (iRow < self.firstChangedRow || iRow > self.lastChangedRow ||
888         iCol < [self getFirstChangedColumnInRow:iRow] ||
889         iCol > [self getLastChangedColumnInRow:iRow] ||
890         (self->changesByRow[iRow][iCol].changeType != CELL_CHANGE_TEXT &&
891          self->changesByRow[iRow][iCol].changeType != CELL_CHANGE_WIPE)) {
892         NSException *exc = [NSException
893                                exceptionWithName:@"NotTextChange"
894                                reason:@"getCellTextChange called for PendingTermChanges"
895                                userInfo:nil];
896         @throw exc;
897     }
898     return self->changesByRow[iRow][iCol].v.txc;
899 }
900
901 - (struct PendingTileChange)getCellTileChange:(int)iCol row:(int)iRow
902 {
903     [self checkColumnIndices:iCol n:1];
904     [self checkRowIndex:iRow];
905     if (iRow < self.firstChangedRow || iRow > self.lastChangedRow ||
906         iCol < [self getFirstChangedColumnInRow:iRow] ||
907         iCol > [self getLastChangedColumnInRow:iRow] ||
908         self->changesByRow[iRow][iCol].changeType != CELL_CHANGE_TILE) {
909         NSException *exc = [NSException
910                                exceptionWithName:@"NotTileChange"
911                                reason:@"getCellTileChange called for PendingTermChanges"
912                                userInfo:nil];
913         @throw exc;
914     }
915     return self->changesByRow[iRow][iCol].v.tic;
916 }
917
918 - (void)checkColumnIndices:(int)iCol n:(int)nCol
919 {
920     if (iCol < 0) {
921         NSException *exc = [NSException
922                                exceptionWithName:@"InvalidColumnIndex"
923                                reason:@"negative column index"
924                                userInfo:nil];
925         @throw exc;
926     }
927     if (iCol >= self.columnCount || iCol + nCol > self.columnCount) {
928         NSException *exc = [NSException
929                                exceptionWithName:@"InvalidColumnIndex"
930                                reason:@"column index exceeds number of columns"
931                                userInfo:nil];
932         @throw exc;
933     }
934     if (nCol <= 0) {
935         NSException *exc = [NSException
936                                exceptionWithName:@"InvalidColumnIndex"
937                                reason:@"empty column range"
938                                userInfo:nil];
939         @throw exc;
940     }
941 }
942
943 - (void)checkRowIndex:(int)iRow
944 {
945     if (iRow < 0) {
946         NSException *exc = [NSException
947                                exceptionWithName:@"InvalidRowIndex"
948                                reason:@"negative row index"
949                                userInfo:nil];
950         @throw exc;
951     }
952     if (iRow >= self.rowCount) {
953         NSException *exc = [NSException
954                                exceptionWithName:@"InvalidRowIndex"
955                                reason:@"row index exceeds number of rows"
956                                userInfo:nil];
957         @throw exc;
958     }
959 }
960
961 @end
962
963
964 /* The max number of glyphs we support.  Currently this only affects
965  * updateGlyphInfo() for the calculation of the tile size, fontAscender,
966  * fontDescender, nColPre, and nColPost.  The rendering in drawWChar() will
967  * work for a glyph not in updateGlyphInfo()'s set, and that is used for
968  * rendering Japanese characters, though there may be clipping or clearing
969  * artifacts because it wasn't included in updateGlyphInfo()'s calculations.
970  */
971 #define GLYPH_COUNT 256
972
973 /* An AngbandContext represents a logical Term (i.e. what Angband thinks is
974  * a window). This typically maps to one NSView, but may map to more than one
975  * NSView (e.g. the Test and real screen saver view). */
976 @interface AngbandContext : NSObject <NSWindowDelegate>
977 {
978 @public
979
980     /* The Angband term */
981     term *terminal;
982
983 @private
984     /* Is the last time we drew, so we can throttle drawing. */
985     CFAbsoluteTime lastRefreshTime;
986
987     /*
988      * Whether we are currently in live resize, which affects how big we
989      * render our image.
990      */
991     int inLiveResize;
992
993     /* Flags whether or not a fullscreen transition is in progress. */
994     BOOL inFullscreenTransition;
995 }
996
997 /* Column and row counts, by default 80 x 24 */
998 @property int cols;
999 @property int rows;
1000
1001 /* The size of the border between the window edge and the contents */
1002 @property (readonly) NSSize borderSize;
1003
1004 /* Our array of views */
1005 @property NSMutableArray *angbandViews;
1006
1007 /* The buffered image */
1008 @property CGLayerRef angbandLayer;
1009
1010 /* The font of this context */
1011 @property NSFont *angbandViewFont;
1012
1013 /* The size of one tile */
1014 @property (readonly) NSSize tileSize;
1015
1016 /* Font's ascender and descender */
1017 @property (readonly) CGFloat fontAscender;
1018 @property (readonly) CGFloat fontDescender;
1019
1020 /*
1021  * These are the number of columns before or after, respectively, a text
1022  * change that may need to be redrawn.
1023  */
1024 @property (readonly) int nColPre;
1025 @property (readonly) int nColPost;
1026
1027 /* If this context owns a window, here it is. */
1028 @property NSWindow *primaryWindow;
1029
1030 /* Is the record of changes to the contents for the next update. */
1031 @property PendingTermChanges *changes;
1032
1033 @property (nonatomic, assign) BOOL hasSubwindowFlags;
1034 @property (nonatomic, assign) BOOL windowVisibilityChecked;
1035
1036 - (void)drawRect:(NSRect)rect inView:(NSView *)view;
1037
1038 /* Called at initialization to set the term */
1039 - (void)setTerm:(term *)t;
1040
1041 /* Called when the context is going down. */
1042 - (void)dispose;
1043
1044 /* Returns the size of the image. */
1045 - (NSSize)imageSize;
1046
1047 /* Return the rect for a tile at given coordinates. */
1048 - (NSRect)rectInImageForTileAtX:(int)x Y:(int)y;
1049
1050 /* Draw the given wide character into the given tile rect. */
1051 - (void)drawWChar:(wchar_t)wchar inRect:(NSRect)tile context:(CGContextRef)ctx;
1052
1053 /* Locks focus on the Angband image, and scales the CTM appropriately. */
1054 - (CGContextRef)lockFocus;
1055
1056 /* Locks focus on the Angband image but does NOT scale the CTM. Appropriate
1057  * for drawing hairlines. */
1058 - (CGContextRef)lockFocusUnscaled;
1059
1060 /* Unlocks focus. */
1061 - (void)unlockFocus;
1062
1063 /* Returns the primary window for this angband context, creating it if
1064  * necessary */
1065 - (NSWindow *)makePrimaryWindow;
1066
1067 /* Called to add a new Angband view */
1068 - (void)addAngbandView:(AngbandView *)view;
1069
1070 /* Make the context aware that one of its views changed size */
1071 - (void)angbandViewDidScale:(AngbandView *)view;
1072
1073 /* Handle becoming the main window */
1074 - (void)windowDidBecomeMain:(NSNotification *)notification;
1075
1076 /* Return whether the context's primary window is ordered in or not */
1077 - (BOOL)isOrderedIn;
1078
1079 /* Return whether the context's primary window is key */
1080 - (BOOL)isMainWindow;
1081
1082 /* Invalidate the whole image */
1083 - (void)setNeedsDisplay:(BOOL)val;
1084
1085 /* Invalidate part of the image, with the rect expressed in base coordinates */
1086 - (void)setNeedsDisplayInBaseRect:(NSRect)rect;
1087
1088 /* Display (flush) our Angband views */
1089 - (void)displayIfNeeded;
1090
1091 /* Resize context to size of contentRect, and optionally save size to
1092  * defaults */
1093 - (void)resizeTerminalWithContentRect: (NSRect)contentRect saveToDefaults: (BOOL)saveToDefaults;
1094
1095 /*
1096  * Change the minimum size for the window associated with the context.
1097  * termIdx is the index for the terminal:  pass it so this function can be
1098  * used when self->terminal has not yet been set.
1099  */
1100 - (void)setMinimumWindowSize:(int)termIdx;
1101
1102 /* Called from the view to indicate that it is starting or ending live resize */
1103 - (void)viewWillStartLiveResize:(AngbandView *)view;
1104 - (void)viewDidEndLiveResize:(AngbandView *)view;
1105 - (void)saveWindowVisibleToDefaults: (BOOL)windowVisible;
1106 - (BOOL)windowVisibleUsingDefaults;
1107
1108 /* Class methods */
1109 /**
1110  * Gets the default font for all contexts.  Currently not declaring this as
1111  * a class property for compatibility with versions of Xcode prior to 8.
1112  */
1113 + (NSFont*)defaultFont;
1114 /**
1115  * Sets the default font for all contexts.
1116  */
1117 + (void)setDefaultFont:(NSFont*)font;
1118
1119 /* Internal method */
1120 - (AngbandView *)activeView;
1121
1122 @end
1123
1124 /**
1125  * Generate a mask for the subwindow flags. The mask is just a safety check to
1126  * make sure that our windows show and hide as expected.  This function allows
1127  * for future changes to the set of flags without needed to update it here
1128  * (unless the underlying types change).
1129  */
1130 u32b AngbandMaskForValidSubwindowFlags(void)
1131 {
1132     int windowFlagBits = sizeof(*(window_flag)) * CHAR_BIT;
1133     int maxBits = MIN( 16, windowFlagBits );
1134     u32b mask = 0;
1135
1136     for( int i = 0; i < maxBits; i++ )
1137     {
1138         if( window_flag_desc[i] != NULL )
1139         {
1140             mask |= (1 << i);
1141         }
1142     }
1143
1144     return mask;
1145 }
1146
1147 /**
1148  * Check for changes in the subwindow flags and update window visibility.
1149  * This seems to be called for every user event, so we don't
1150  * want to do any unnecessary hiding or showing of windows.
1151  */
1152 static void AngbandUpdateWindowVisibility(void)
1153 {
1154     /* Because this function is called frequently, we'll make the mask static.
1155          * It doesn't change between calls, as the flags themselves are hardcoded */
1156     static u32b validWindowFlagsMask = 0;
1157
1158     if( validWindowFlagsMask == 0 )
1159     {
1160         validWindowFlagsMask = AngbandMaskForValidSubwindowFlags();
1161     }
1162
1163     /* Loop through all of the subwindows and see if there is a change in the
1164          * flags. If so, show or hide the corresponding window. We don't care about
1165          * the flags themselves; we just want to know if any are set. */
1166     for( int i = 1; i < ANGBAND_TERM_MAX; i++ )
1167     {
1168         AngbandContext *angbandContext =
1169             (__bridge AngbandContext*) (angband_term[i]->data);
1170
1171         if( angbandContext == nil )
1172         {
1173             continue;
1174         }
1175
1176         /* This horrible mess of flags is so that we can try to maintain some
1177                  * user visibility preference. This should allow the user a window and
1178                  * have it stay closed between application launches. However, this
1179                  * means that when a subwindow is turned on, it will no longer appear
1180                  * automatically. Angband has no concept of user control over window
1181                  * visibility, other than the subwindow flags. */
1182         if( !angbandContext.windowVisibilityChecked )
1183         {
1184             if( [angbandContext windowVisibleUsingDefaults] )
1185             {
1186                 [angbandContext.primaryWindow orderFront: nil];
1187                 angbandContext.windowVisibilityChecked = YES;
1188             }
1189             else
1190             {
1191                 [angbandContext.primaryWindow close];
1192                 angbandContext.windowVisibilityChecked = NO;
1193             }
1194         }
1195         else
1196         {
1197             BOOL termHasSubwindowFlags = ((window_flag[i] & validWindowFlagsMask) > 0);
1198
1199             if( angbandContext.hasSubwindowFlags && !termHasSubwindowFlags )
1200             {
1201                 [angbandContext.primaryWindow close];
1202                 angbandContext.hasSubwindowFlags = NO;
1203                 [angbandContext saveWindowVisibleToDefaults: NO];
1204             }
1205             else if( !angbandContext.hasSubwindowFlags && termHasSubwindowFlags )
1206             {
1207                 [angbandContext.primaryWindow orderFront: nil];
1208                 angbandContext.hasSubwindowFlags = YES;
1209                 [angbandContext saveWindowVisibleToDefaults: YES];
1210             }
1211         }
1212     }
1213
1214     /* Make the main window key so that user events go to the right spot */
1215     AngbandContext *mainWindow =
1216         (__bridge AngbandContext*) (angband_term[0]->data);
1217     [mainWindow.primaryWindow makeKeyAndOrderFront: nil];
1218 }
1219
1220
1221 /**
1222  * ------------------------------------------------------------------------
1223  * Graphics support
1224  * ------------------------------------------------------------------------ */
1225
1226 /**
1227  * The tile image
1228  */
1229 static CGImageRef pict_image;
1230
1231 /**
1232  * Numbers of rows and columns in a tileset,
1233  * calculated by the PICT/PNG loading code
1234  */
1235 static int pict_cols = 0;
1236 static int pict_rows = 0;
1237
1238 /**
1239  * Requested graphics mode (as a grafID).
1240  * The current mode is stored in current_graphics_mode.
1241  */
1242 static int graf_mode_req = 0;
1243
1244 /**
1245  * Helper function to check the various ways that graphics can be enabled,
1246  * guarding against NULL
1247  */
1248 static BOOL graphics_are_enabled(void)
1249 {
1250     return current_graphics_mode
1251         && current_graphics_mode->grafID != GRAPHICS_NONE;
1252 }
1253
1254 /**
1255  * Hack -- game in progress
1256  */
1257 static Boolean game_in_progress = FALSE;
1258
1259
1260 #pragma mark Prototypes
1261 static void wakeup_event_loop(void);
1262 static void hook_plog(const char *str);
1263 static void hook_quit(const char * str);
1264 static NSString* get_lib_directory(void);
1265 static NSString* get_doc_directory(void);
1266 static NSString* AngbandCorrectedDirectoryPath(NSString *originalPath);
1267 static void prepare_paths_and_directories(void);
1268 static void handle_open_when_ready(void);
1269 static void play_sound(int event);
1270 static BOOL check_events(int wait);
1271 static BOOL send_event(NSEvent *event);
1272 static void record_current_savefile(void);
1273 #ifdef JP
1274 static wchar_t convert_two_byte_eucjp_to_utf16_native(const char *cp);
1275 #endif
1276
1277 /**
1278  * Available values for 'wait'
1279  */
1280 #define CHECK_EVENTS_DRAIN -1
1281 #define CHECK_EVENTS_NO_WAIT    0
1282 #define CHECK_EVENTS_WAIT 1
1283
1284
1285 /**
1286  * Note when "open"/"new" become valid
1287  */
1288 static bool initialized = FALSE;
1289
1290 /* Methods for getting the appropriate NSUserDefaults */
1291 @interface NSUserDefaults (AngbandDefaults)
1292 + (NSUserDefaults *)angbandDefaults;
1293 @end
1294
1295 @implementation NSUserDefaults (AngbandDefaults)
1296 + (NSUserDefaults *)angbandDefaults
1297 {
1298     return [NSUserDefaults standardUserDefaults];
1299 }
1300 @end
1301
1302 /* Methods for pulling images out of the Angband bundle (which may be separate
1303  * from the current bundle in the case of a screensaver */
1304 @interface NSImage (AngbandImages)
1305 + (NSImage *)angbandImage:(NSString *)name;
1306 @end
1307
1308 /* The NSView subclass that draws our Angband image */
1309 @interface AngbandView : NSView
1310 {
1311     AngbandContext *angbandContext;
1312 }
1313
1314 - (void)setAngbandContext:(AngbandContext *)context;
1315 - (AngbandContext *)angbandContext;
1316
1317 @end
1318
1319 @implementation NSImage (AngbandImages)
1320
1321 /* Returns an image in the resource directoy of the bundle containing the
1322  * Angband view class. */
1323 + (NSImage *)angbandImage:(NSString *)name
1324 {
1325     NSBundle *bundle = [NSBundle bundleForClass:[AngbandView class]];
1326     NSString *path = [bundle pathForImageResource:name];
1327     return (path) ? [[NSImage alloc] initByReferencingFile:path] : nil;
1328 }
1329
1330 @end
1331
1332
1333 @implementation AngbandContext
1334
1335 @synthesize hasSubwindowFlags=_hasSubwindowFlags;
1336 @synthesize windowVisibilityChecked=_windowVisibilityChecked;
1337
1338 - (BOOL)useLiveResizeOptimization
1339 {
1340     /* If we have graphics turned off, text rendering is fast enough that we
1341          * don't need to use a live resize optimization. */
1342     return self->inLiveResize && graphics_are_enabled();
1343 }
1344
1345 - (NSSize)baseSize
1346 {
1347     /* We round the base size down. If we round it up, I believe we may end up
1348          * with pixels that nobody "owns" that may accumulate garbage. In general
1349          * rounding down is harmless, because any lost pixels may be sopped up by
1350          * the border. */
1351     return NSMakeSize(
1352         floor(self.cols * self.tileSize.width + 2 * self.borderSize.width),
1353         floor(self.rows * self.tileSize.height + 2 * self.borderSize.height));
1354 }
1355
1356 /* qsort-compatible compare function for CGSizes */
1357 static int compare_advances(const void *ap, const void *bp)
1358 {
1359     const CGSize *a = ap, *b = bp;
1360     return (a->width > b->width) - (a->width < b->width);
1361 }
1362
1363 /**
1364  * Precompute certain metrics (tileSize, fontAscender, fontDescender, nColPre,
1365  * and nColPost) for the current font.
1366  */
1367 - (void)updateGlyphInfo
1368 {
1369     NSFont *screenFont = [self.angbandViewFont screenFont];
1370
1371     /* Generate a string containing each MacRoman character */
1372     /*
1373      * Here and below, dynamically allocate working arrays rather than put them
1374      * on the stack in case limited stack space is an issue.
1375      */
1376     unsigned char *latinString = malloc(GLYPH_COUNT);
1377     if (latinString == 0) {
1378         NSException *exc = [NSException exceptionWithName:@"OutOfMemory"
1379                                         reason:@"latinString in updateGlyphInfo"
1380                                         userInfo:nil];
1381         @throw exc;
1382     }
1383     size_t i;
1384     for (i=0; i < GLYPH_COUNT; i++) latinString[i] = (unsigned char)i;
1385
1386     /* Turn that into unichar. Angband uses ISO Latin 1. */
1387     NSString *allCharsString = [[NSString alloc] initWithBytes:latinString length:sizeof latinString encoding:NSISOLatin1StringEncoding];
1388     unichar *unicharString = malloc(GLYPH_COUNT * sizeof(unichar));
1389     if (unicharString == 0) {
1390         free(latinString);
1391         NSException *exc = [NSException exceptionWithName:@"OutOfMemory"
1392                                         reason:@"unicharString in updateGlyphInfo"
1393                                         userInfo:nil];
1394         @throw exc;
1395     }
1396     unicharString[0] = 0;
1397     [allCharsString getCharacters:unicharString range:NSMakeRange(0, MIN(GLYPH_COUNT, [allCharsString length]))];
1398     allCharsString = nil;
1399     free(latinString);
1400
1401     /* Get glyphs */
1402     CGGlyph *glyphArray = calloc(GLYPH_COUNT, sizeof(CGGlyph));
1403     if (glyphArray == 0) {
1404         free(unicharString);
1405         NSException *exc = [NSException exceptionWithName:@"OutOfMemory"
1406                                         reason:@"glyphArray in updateGlyphInfo"
1407                                         userInfo:nil];
1408         @throw exc;
1409     }
1410     CTFontGetGlyphsForCharacters((CTFontRef)screenFont, unicharString,
1411                                  glyphArray, GLYPH_COUNT);
1412     free(unicharString);
1413
1414     /* Get advances. Record the max advance. */
1415     CGSize *advances = malloc(GLYPH_COUNT * sizeof(CGSize));
1416     if (advances == 0) {
1417         free(glyphArray);
1418         NSException *exc = [NSException exceptionWithName:@"OutOfMemory"
1419                                         reason:@"advances in updateGlyphInfo"
1420                                         userInfo:nil];
1421         @throw exc;
1422     }
1423     CTFontGetAdvancesForGlyphs(
1424         (CTFontRef)screenFont, kCTFontHorizontalOrientation, glyphArray,
1425         advances, GLYPH_COUNT);
1426     CGFloat *glyphWidths = malloc(GLYPH_COUNT * sizeof(CGFloat));
1427     if (glyphWidths == 0) {
1428         free(glyphArray);
1429         free(advances);
1430         NSException *exc = [NSException exceptionWithName:@"OutOfMemory"
1431                                         reason:@"glyphWidths in updateGlyphInfo"
1432                                         userInfo:nil];
1433         @throw exc;
1434     }
1435     for (i=0; i < GLYPH_COUNT; i++) {
1436         glyphWidths[i] = advances[i].width;
1437     }
1438
1439     /* For good non-mono-font support, use the median advance. Start by sorting
1440          * all advances. */
1441     qsort(advances, GLYPH_COUNT, sizeof *advances, compare_advances);
1442
1443     /* Skip over any initially empty run */
1444     size_t startIdx;
1445     for (startIdx = 0; startIdx < GLYPH_COUNT; startIdx++)
1446     {
1447         if (advances[startIdx].width > 0) break;
1448     }
1449
1450     /* Pick the center to find the median */
1451     CGFloat medianAdvance = 0;
1452     if (startIdx < GLYPH_COUNT)
1453     {
1454                 /* In case we have all zero advances for some reason */
1455         medianAdvance = advances[(startIdx + GLYPH_COUNT)/2].width;
1456     }
1457
1458     free(advances);
1459
1460     /*
1461      * Record the ascender and descender.  Some fonts, for instance DIN
1462      * Condensed and Rockwell in 10.14, the ascent on '@' exceeds that
1463      * reported by [screenFont ascender].  Get the overall bounding box
1464      * for the glyphs and use that instead of the ascender and descender
1465      * values if the bounding box result extends farther from the baseline.
1466      */
1467     CGRect bounds = CTFontGetBoundingRectsForGlyphs(
1468         (CTFontRef) screenFont, kCTFontHorizontalOrientation, glyphArray,
1469         NULL, GLYPH_COUNT);
1470     self->_fontAscender = [screenFont ascender];
1471     if (self->_fontAscender < bounds.origin.y + bounds.size.height) {
1472         self->_fontAscender = bounds.origin.y + bounds.size.height;
1473     }
1474     self->_fontDescender = [screenFont descender];
1475     if (self->_fontDescender > bounds.origin.y) {
1476         self->_fontDescender = bounds.origin.y;
1477     }
1478
1479     /*
1480      * Record the tile size.  Round both values up to have tile boundaries
1481      * match pixel boundaries.
1482      */
1483     self->_tileSize.width = ceil(medianAdvance);
1484     self->_tileSize.height = ceil(self.fontAscender - self.fontDescender);
1485
1486     /*
1487      * Determine whether neighboring columns need to be redrawn when a
1488      * character changes.
1489      */
1490     CGRect *boxes = malloc(GLYPH_COUNT * sizeof(CGRect));
1491     if (boxes == 0) {
1492         free(glyphWidths);
1493         free(glyphArray);
1494         NSException *exc = [NSException exceptionWithName:@"OutOfMemory"
1495                                         reason:@"boxes in updateGlyphInfo"
1496                                         userInfo:nil];
1497         @throw exc;
1498     }
1499     CGFloat beyond_right = 0.;
1500     CGFloat beyond_left = 0.;
1501     CTFontGetBoundingRectsForGlyphs(
1502         (CTFontRef)screenFont,
1503         kCTFontHorizontalOrientation,
1504         glyphArray,
1505         boxes,
1506         GLYPH_COUNT);
1507     for (i = 0; i < GLYPH_COUNT; i++) {
1508         /* Account for the compression and offset used by drawWChar(). */
1509         CGFloat compression, offset;
1510         CGFloat v;
1511
1512         if (glyphWidths[i] <= self.tileSize.width) {
1513             compression = 1.;
1514             offset = 0.5 * (self.tileSize.width - glyphWidths[i]);
1515         } else {
1516             compression = self.tileSize.width / glyphWidths[i];
1517             offset = 0.;
1518         }
1519         v = (offset + boxes[i].origin.x) * compression;
1520         if (beyond_left > v) {
1521             beyond_left = v;
1522         }
1523         v = (offset + boxes[i].origin.x + boxes[i].size.width) * compression;
1524         if (beyond_right < v) {
1525             beyond_right = v;
1526         }
1527     }
1528     free(boxes);
1529     self->_nColPre = ceil(-beyond_left / self.tileSize.width);
1530     if (beyond_right > self.tileSize.width) {
1531         self->_nColPost =
1532             ceil((beyond_right - self.tileSize.width) / self.tileSize.width);
1533     } else {
1534         self->_nColPost = 0;
1535     }
1536
1537     free(glyphWidths);
1538     free(glyphArray);
1539 }
1540
1541 - (void)updateImage
1542 {
1543     NSSize size = NSMakeSize(1, 1);
1544     
1545     AngbandView *activeView = [self activeView];
1546     if (activeView)
1547     {
1548         /* If we are in live resize, draw as big as the screen, so we can scale
1549                  * nicely to any size. If we are not in live resize, then use the
1550                  * bounds of the active view. */
1551         NSScreen *screen;
1552         if ([self useLiveResizeOptimization] && (screen = [[activeView window] screen]) != NULL)
1553         {
1554             size = [screen frame].size;
1555         }
1556         else
1557         {
1558             size = [activeView bounds].size;
1559         }
1560     }
1561
1562     CGLayerRelease(self.angbandLayer);
1563     
1564     /* Use the highest monitor scale factor on the system to work out what
1565      * scale to draw at - not the recommended method, but works where we
1566      * can't easily get the monitor the current draw is occurring on. */
1567     float angbandLayerScale = 1.0;
1568     if ([[NSScreen mainScreen] respondsToSelector:@selector(backingScaleFactor)]) {
1569         for (NSScreen *screen in [NSScreen screens]) {
1570             angbandLayerScale = fmax(angbandLayerScale, [screen backingScaleFactor]);
1571         }
1572     }
1573
1574     /* Make a bitmap context as an example for our layer */
1575     CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
1576     CGContextRef exampleCtx = CGBitmapContextCreate(NULL, 1, 1, 8 /* bits per component */, 48 /* bytesPerRow */, cs, kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host);
1577     CGColorSpaceRelease(cs);
1578
1579     /* Create the layer at the appropriate size */
1580     size.width = fmax(1, ceil(size.width * angbandLayerScale));
1581     size.height = fmax(1, ceil(size.height * angbandLayerScale));
1582     self.angbandLayer =
1583         CGLayerCreateWithContext(exampleCtx, *(CGSize *)&size, NULL);
1584
1585     CFRelease(exampleCtx);
1586
1587     /* Set the new context of the layer to draw at the correct scale */
1588     CGContextRef ctx = CGLayerGetContext(self.angbandLayer);
1589     CGContextScaleCTM(ctx, angbandLayerScale, angbandLayerScale);
1590
1591     [self lockFocus];
1592     [[NSColor blackColor] set];
1593     NSRectFill((NSRect){NSZeroPoint, [self baseSize]});
1594     [self unlockFocus];
1595 }
1596
1597 - (void)requestRedraw
1598 {
1599     if (! self->terminal) return;
1600     
1601     term *old = Term;
1602     
1603     /* Activate the term */
1604     Term_activate(self->terminal);
1605     
1606     /* Redraw the contents */
1607     Term_redraw();
1608     
1609     /* Flush the output */
1610     Term_fresh();
1611     
1612     /* Restore the old term */
1613     Term_activate(old);
1614 }
1615
1616 - (void)setTerm:(term *)t
1617 {
1618     self->terminal = t;
1619 }
1620
1621 - (void)viewWillStartLiveResize:(AngbandView *)view
1622 {
1623 #if USE_LIVE_RESIZE_CACHE
1624     if (self->inLiveResize < INT_MAX) self->inLiveResize++;
1625     else [NSException raise:NSInternalInconsistencyException format:@"inLiveResize overflow"];
1626     
1627     if (self->inLiveResize == 1 && graphics_are_enabled())
1628     {
1629         [self updateImage];
1630         
1631         [self setNeedsDisplay:YES]; /* We'll need to redisplay everything anyways, so avoid creating all those little redisplay rects */
1632         [self requestRedraw];
1633     }
1634 #endif
1635 }
1636
1637 - (void)viewDidEndLiveResize:(AngbandView *)view
1638 {
1639 #if USE_LIVE_RESIZE_CACHE
1640     if (self->inLiveResize > 0) self->inLiveResize--;
1641     else [NSException raise:NSInternalInconsistencyException format:@"inLiveResize underflow"];
1642     
1643     if (self->inLiveResize == 0 && graphics_are_enabled())
1644     {
1645         [self updateImage];
1646         
1647         [self setNeedsDisplay:YES]; /* We'll need to redisplay everything anyways, so avoid creating all those little redisplay rects */
1648         [self requestRedraw];
1649     }
1650 #endif
1651 }
1652
1653 /**
1654  * If we're trying to limit ourselves to a certain number of frames per second,
1655  * then compute how long it's been since we last drew, and then wait until the
1656  * next frame has passed. */
1657 - (void)throttle
1658 {
1659     if (frames_per_second > 0)
1660     {
1661         CFAbsoluteTime now = CFAbsoluteTimeGetCurrent();
1662         CFTimeInterval timeSinceLastRefresh = now - self->lastRefreshTime;
1663         CFTimeInterval timeUntilNextRefresh = (1. / (double)frames_per_second) - timeSinceLastRefresh;
1664         
1665         if (timeUntilNextRefresh > 0)
1666         {
1667             usleep((unsigned long)(timeUntilNextRefresh * 1000000.));
1668         }
1669     }
1670     self->lastRefreshTime = CFAbsoluteTimeGetCurrent();
1671 }
1672
1673 - (void)drawWChar:(wchar_t)wchar inRect:(NSRect)tile context:(CGContextRef)ctx
1674 {
1675     CGFloat tileOffsetY = self.fontAscender;
1676     CGFloat tileOffsetX = 0.0;
1677     NSFont *screenFont = [self.angbandViewFont screenFont];
1678     UniChar unicharString[2] = {(UniChar)wchar, 0};
1679
1680     /* Get glyph and advance */
1681     CGGlyph thisGlyphArray[1] = { 0 };
1682     CGSize advances[1] = { { 0, 0 } };
1683     CTFontGetGlyphsForCharacters((CTFontRef)screenFont, unicharString, thisGlyphArray, 1);
1684     CGGlyph glyph = thisGlyphArray[0];
1685     CTFontGetAdvancesForGlyphs((CTFontRef)screenFont, kCTFontHorizontalOrientation, thisGlyphArray, advances, 1);
1686     CGSize advance = advances[0];
1687     
1688     /* If our font is not monospaced, our tile width is deliberately not big
1689          * enough for every character. In that event, if our glyph is too wide, we
1690          * need to compress it horizontally. Compute the compression ratio.
1691          * 1.0 means no compression. */
1692     double compressionRatio;
1693     if (advance.width <= NSWidth(tile))
1694     {
1695         /* Our glyph fits, so we can just draw it, possibly with an offset */
1696         compressionRatio = 1.0;
1697         tileOffsetX = (NSWidth(tile) - advance.width)/2;
1698     }
1699     else
1700     {
1701         /* Our glyph doesn't fit, so we'll have to compress it */
1702         compressionRatio = NSWidth(tile) / advance.width;
1703         tileOffsetX = 0;
1704     }
1705
1706     
1707     /* Now draw it */
1708     CGAffineTransform textMatrix = CGContextGetTextMatrix(ctx);
1709     CGFloat savedA = textMatrix.a;
1710
1711     /* Set the position */
1712     textMatrix.tx = tile.origin.x + tileOffsetX;
1713     textMatrix.ty = tile.origin.y + tileOffsetY;
1714
1715     /* Maybe squish it horizontally. */
1716     if (compressionRatio != 1.)
1717     {
1718         textMatrix.a *= compressionRatio;
1719     }
1720
1721     textMatrix = CGAffineTransformScale( textMatrix, 1.0, -1.0 );
1722     CGContextSetTextMatrix(ctx, textMatrix);
1723     CGContextShowGlyphsAtPositions(ctx, &glyph, &CGPointZero, 1);
1724     
1725     /* Restore the text matrix if we messed with the compression ratio */
1726     if (compressionRatio != 1.)
1727     {
1728         textMatrix.a = savedA;
1729         CGContextSetTextMatrix(ctx, textMatrix);
1730     }
1731
1732     textMatrix = CGAffineTransformScale( textMatrix, 1.0, -1.0 );
1733     CGContextSetTextMatrix(ctx, textMatrix);
1734 }
1735
1736 /* Lock and unlock focus on our image or layer, setting up the CTM
1737  * appropriately. */
1738 - (CGContextRef)lockFocusUnscaled
1739 {
1740     /* Create an NSGraphicsContext representing this CGLayer */
1741     CGContextRef ctx = CGLayerGetContext(self.angbandLayer);
1742     NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithGraphicsPort:ctx flipped:NO];
1743     [NSGraphicsContext saveGraphicsState];
1744     [NSGraphicsContext setCurrentContext:context];
1745     CGContextSaveGState(ctx);
1746     return ctx;
1747 }
1748
1749 - (void)unlockFocus
1750 {
1751     /* Restore the graphics state */
1752     CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort];
1753     CGContextRestoreGState(ctx);
1754     [NSGraphicsContext restoreGraphicsState];
1755 }
1756
1757 - (NSSize)imageSize
1758 {
1759     /* Return the size of our layer */
1760     CGSize result = CGLayerGetSize(self.angbandLayer);
1761     return NSMakeSize(result.width, result.height);
1762 }
1763
1764 - (CGContextRef)lockFocus
1765 {
1766     return [self lockFocusUnscaled];
1767 }
1768
1769
1770 - (NSRect)rectInImageForTileAtX:(int)x Y:(int)y
1771 {
1772     int flippedY = y;
1773     return NSMakeRect(x * self.tileSize.width + self.borderSize.width,
1774                       flippedY * self.tileSize.height + self.borderSize.height,
1775                       self.tileSize.width, self.tileSize.height);
1776 }
1777
1778 - (void)setSelectionFont:(NSFont*)font adjustTerminal: (BOOL)adjustTerminal
1779 {
1780     /* Record the new font */
1781     self.angbandViewFont = font;
1782
1783     /* Update our glyph info */
1784     [self updateGlyphInfo];
1785
1786     if( adjustTerminal )
1787     {
1788         /* Adjust terminal to fit window with new font; save the new columns
1789                  * and rows since they could be changed */
1790         NSRect contentRect =
1791             [self.primaryWindow
1792                  contentRectForFrameRect: [self.primaryWindow frame]];
1793
1794         [self setMinimumWindowSize:[self terminalIndex]];
1795         NSSize size = self.primaryWindow.contentMinSize;
1796         BOOL windowNeedsResizing = NO;
1797         if (contentRect.size.width < size.width) {
1798             contentRect.size.width = size.width;
1799             windowNeedsResizing = YES;
1800         }
1801         if (contentRect.size.height < size.height) {
1802             contentRect.size.height = size.height;
1803             windowNeedsResizing = YES;
1804         }
1805         if (windowNeedsResizing) {
1806             size.width = contentRect.size.width;
1807             size.height = contentRect.size.height;
1808             [self.primaryWindow setContentSize:size];
1809         }
1810         [self resizeTerminalWithContentRect: contentRect saveToDefaults: YES];
1811     }
1812
1813     /* Update our image */
1814     [self updateImage];
1815 }
1816
1817 - (id)init
1818 {
1819     if ((self = [super init]))
1820     {
1821         /* Default rows and cols */
1822         self->_cols = 80;
1823         self->_rows = 24;
1824
1825         /* Default border size */
1826         self->_borderSize = NSMakeSize(2, 2);
1827
1828         /* Allocate our array of views */
1829         self->_angbandViews = [[NSMutableArray alloc] init];
1830
1831         self->_nColPre = 0;
1832         self->_nColPost = 0;
1833
1834         self->_changes =
1835             [[PendingTermChanges alloc] initWithColumnsRows:self->_cols
1836                                         rows:self->_rows];
1837         self->lastRefreshTime = CFAbsoluteTimeGetCurrent();
1838         self->inLiveResize = 0;
1839         self->inFullscreenTransition = NO;
1840
1841         /* Make the image. Since we have no views, it'll just be a puny 1x1 image. */
1842         [self updateImage];
1843
1844         self->_windowVisibilityChecked = NO;
1845     }
1846     return self;
1847 }
1848
1849 /**
1850  * Destroy all the receiver's stuff. This is intended to be callable more than
1851  * once.
1852  */
1853 - (void)dispose
1854 {
1855     self->terminal = NULL;
1856
1857     /* Disassociate ourselves from our angbandViews */
1858     [self.angbandViews makeObjectsPerformSelector:@selector(setAngbandContext:) withObject:nil];
1859     self.angbandViews = nil;
1860
1861     /* Destroy the layer/image */
1862     CGLayerRelease(self.angbandLayer);
1863     self.angbandLayer = NULL;
1864
1865     /* Font */
1866     self.angbandViewFont = nil;
1867
1868     /* Window */
1869     [self.primaryWindow setDelegate:nil];
1870     [self.primaryWindow close];
1871     self.primaryWindow = nil;
1872
1873     /* Pending changes */
1874     self.changes = nil;
1875 }
1876
1877 /* Usual Cocoa fare */
1878 - (void)dealloc
1879 {
1880     [self dispose];
1881 }
1882
1883 #if 0
1884 /* From the Linux mbstowcs(3) man page:
1885  *   If dest is NULL, n is ignored, and the conversion  proceeds  as  above,
1886  *   except  that  the converted wide characters are not written out to mem‐
1887  *   ory, and that no length limit exists.
1888  */
1889 static size_t Term_mbcs_cocoa(wchar_t *dest, const char *src, int n)
1890 {
1891     int i;
1892     int count = 0;
1893
1894     /* Unicode code point to UTF-8
1895      *  0x0000-0x007f:   0xxxxxxx
1896      *  0x0080-0x07ff:   110xxxxx 10xxxxxx
1897      *  0x0800-0xffff:   1110xxxx 10xxxxxx 10xxxxxx
1898      * 0x10000-0x1fffff: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
1899      * Note that UTF-16 limits Unicode to 0x10ffff. This code is not
1900      * endian-agnostic.
1901      */
1902     for (i = 0; i < n || dest == NULL; i++) {
1903         if ((src[i] & 0x80) == 0) {
1904             if (dest != NULL) dest[count] = src[i];
1905             if (src[i] == 0) break;
1906         } else if ((src[i] & 0xe0) == 0xc0) {
1907             if (dest != NULL) dest[count] = 
1908                             (((unsigned char)src[i] & 0x1f) << 6)| 
1909                             ((unsigned char)src[i+1] & 0x3f);
1910             i++;
1911         } else if ((src[i] & 0xf0) == 0xe0) {
1912             if (dest != NULL) dest[count] = 
1913                             (((unsigned char)src[i] & 0x0f) << 12) | 
1914                             (((unsigned char)src[i+1] & 0x3f) << 6) |
1915                             ((unsigned char)src[i+2] & 0x3f);
1916             i += 2;
1917         } else if ((src[i] & 0xf8) == 0xf0) {
1918             if (dest != NULL) dest[count] = 
1919                             (((unsigned char)src[i] & 0x0f) << 18) | 
1920                             (((unsigned char)src[i+1] & 0x3f) << 12) |
1921                             (((unsigned char)src[i+2] & 0x3f) << 6) |
1922                             ((unsigned char)src[i+3] & 0x3f);
1923             i += 3;
1924         } else {
1925             /* Found an invalid multibyte sequence */
1926             return (size_t)-1;
1927         }
1928         count++;
1929     }
1930     return count;
1931 }
1932 #endif
1933
1934 - (void)addAngbandView:(AngbandView *)view
1935 {
1936     if (! [self.angbandViews containsObject:view])
1937     {
1938         [self.angbandViews addObject:view];
1939         [self updateImage];
1940         [self setNeedsDisplay:YES]; /* We'll need to redisplay everything anyways, so avoid creating all those little redisplay rects */
1941         [self requestRedraw];
1942     }
1943 }
1944
1945 /**
1946  * For defaultFont and setDefaultFont.
1947  */
1948 static __strong NSFont* gDefaultFont = nil;
1949
1950 + (NSFont*)defaultFont
1951 {
1952     return gDefaultFont;
1953 }
1954
1955 + (void)setDefaultFont:(NSFont*)font
1956 {
1957     gDefaultFont = font;
1958 }
1959
1960 /**
1961  * We have this notion of an "active" AngbandView, which is the largest - the
1962  * idea being that in the screen saver, when the user hits Test in System
1963  * Preferences, we don't want to keep driving the AngbandView in the
1964  * background.  Our active AngbandView is the widest - that's a hack all right.
1965  * Mercifully when we're just playing the game there's only one view.
1966  */
1967 - (AngbandView *)activeView
1968 {
1969     if ([self.angbandViews count] == 1)
1970         return [self.angbandViews objectAtIndex:0];
1971
1972     AngbandView *result = nil;
1973     float maxWidth = 0;
1974     for (AngbandView *angbandView in self.angbandViews)
1975     {
1976         float width = [angbandView frame].size.width;
1977         if (width > maxWidth)
1978         {
1979             maxWidth = width;
1980             result = angbandView;
1981         }
1982     }
1983     return result;
1984 }
1985
1986 - (void)angbandViewDidScale:(AngbandView *)view
1987 {
1988     /* If we're live-resizing with graphics, we're using the live resize
1989          * optimization, so don't update the image. Otherwise do it. */
1990     if (! (self->inLiveResize && graphics_are_enabled()) && view == [self activeView])
1991     {
1992         [self updateImage];
1993         
1994         [self setNeedsDisplay:YES]; /*we'll need to redisplay everything anyways, so avoid creating all those little redisplay rects */
1995         [self requestRedraw];
1996     }
1997 }
1998
1999
2000 - (void)removeAngbandView:(AngbandView *)view
2001 {
2002     if ([self.angbandViews containsObject:view])
2003     {
2004         [self.angbandViews removeObject:view];
2005         [self updateImage];
2006         [self setNeedsDisplay:YES]; /* We'll need to redisplay everything anyways, so avoid creating all those little redisplay rects */
2007         if ([self.angbandViews count]) [self requestRedraw];
2008     }
2009 }
2010
2011
2012 - (NSWindow *)makePrimaryWindow
2013 {
2014     if (! self.primaryWindow)
2015     {
2016         /* This has to be done after the font is set, which it already is in
2017                  * term_init_cocoa() */
2018         NSSize sz = self.baseSize;
2019         NSRect contentRect = NSMakeRect( 0.0, 0.0, sz.width, sz.height );
2020
2021         NSUInteger styleMask = NSTitledWindowMask | NSResizableWindowMask | NSMiniaturizableWindowMask;
2022
2023         /* Make every window other than the main window closable */
2024         if ((__bridge AngbandContext*) (angband_term[0]->data) != self)
2025         {
2026             styleMask |= NSClosableWindowMask;
2027         }
2028
2029         self.primaryWindow = [[NSWindow alloc] initWithContentRect:contentRect styleMask: styleMask backing:NSBackingStoreBuffered defer:YES];
2030
2031         /* Not to be released when closed */
2032         [self.primaryWindow setReleasedWhenClosed:NO];
2033         [self.primaryWindow setExcludedFromWindowsMenu: YES]; /* we're using custom window menu handling */
2034
2035         /* Make the view */
2036         AngbandView *angbandView = [[AngbandView alloc] initWithFrame:contentRect];
2037         [angbandView setAngbandContext:self];
2038         [self.angbandViews addObject:angbandView];
2039         [self.primaryWindow setContentView:angbandView];
2040
2041         /* We are its delegate */
2042         [self.primaryWindow setDelegate:self];
2043
2044         /* Update our image, since this is probably the first angband view
2045                  * we've gotten. */
2046         [self updateImage];
2047     }
2048     return self.primaryWindow;
2049 }
2050
2051
2052
2053 #pragma mark View/Window Passthrough
2054
2055 /**
2056  * This is what our views call to get us to draw to the window
2057  */
2058 - (void)drawRect:(NSRect)rect inView:(NSView *)view
2059 {
2060     /* Take this opportunity to throttle so we don't flush faster than desired.
2061          */
2062     BOOL viewInLiveResize = [view inLiveResize];
2063     if (! viewInLiveResize) [self throttle];
2064
2065     /* With a GLayer, use CGContextDrawLayerInRect */
2066     CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort];
2067     NSRect bounds = [view bounds];
2068     if (viewInLiveResize) CGContextSetInterpolationQuality(context, kCGInterpolationLow);
2069     CGContextSetBlendMode(context, kCGBlendModeCopy);
2070     CGContextDrawLayerInRect(context, *(CGRect *)&bounds, self.angbandLayer);
2071     if (viewInLiveResize) CGContextSetInterpolationQuality(context, kCGInterpolationDefault);
2072 }
2073
2074 - (BOOL)isOrderedIn
2075 {
2076     return [[[self.angbandViews lastObject] window] isVisible];
2077 }
2078
2079 - (BOOL)isMainWindow
2080 {
2081     return [[[self.angbandViews lastObject] window] isMainWindow];
2082 }
2083
2084 - (void)setNeedsDisplay:(BOOL)val
2085 {
2086     for (NSView *angbandView in self.angbandViews)
2087     {
2088         [angbandView setNeedsDisplay:val];
2089     }
2090 }
2091
2092 - (void)setNeedsDisplayInBaseRect:(NSRect)rect
2093 {
2094     for (NSView *angbandView in self.angbandViews)
2095     {
2096         [angbandView setNeedsDisplayInRect: rect];
2097     }
2098 }
2099
2100 - (void)displayIfNeeded
2101 {
2102     [[self activeView] displayIfNeeded];
2103 }
2104
2105 - (int)terminalIndex
2106 {
2107         int termIndex = 0;
2108
2109         for( termIndex = 0; termIndex < ANGBAND_TERM_MAX; termIndex++ )
2110         {
2111                 if( angband_term[termIndex] == self->terminal )
2112                 {
2113                         break;
2114                 }
2115         }
2116
2117         return termIndex;
2118 }
2119
2120 - (void)resizeTerminalWithContentRect: (NSRect)contentRect saveToDefaults: (BOOL)saveToDefaults
2121 {
2122     CGFloat newRows = floor(
2123         (contentRect.size.height - (self.borderSize.height * 2.0)) /
2124         self.tileSize.height);
2125     CGFloat newColumns = ceil(
2126         (contentRect.size.width - (self.borderSize.width * 2.0)) /
2127         self.tileSize.width);
2128
2129     if (newRows < 1 || newColumns < 1) return;
2130     self->_cols = newColumns;
2131     self->_rows = newRows;
2132     [self.changes resize:self.cols rows:self.rows];
2133
2134     if( saveToDefaults )
2135     {
2136         int termIndex = [self terminalIndex];
2137         NSArray *terminals = [[NSUserDefaults standardUserDefaults] valueForKey: AngbandTerminalsDefaultsKey];
2138
2139         if( termIndex < (int)[terminals count] )
2140         {
2141             NSMutableDictionary *mutableTerm = [[NSMutableDictionary alloc] initWithDictionary: [terminals objectAtIndex: termIndex]];
2142             [mutableTerm setValue: [NSNumber numberWithInteger: self.cols]
2143                          forKey: AngbandTerminalColumnsDefaultsKey];
2144             [mutableTerm setValue: [NSNumber numberWithInteger: self.rows]
2145                          forKey: AngbandTerminalRowsDefaultsKey];
2146
2147             NSMutableArray *mutableTerminals = [[NSMutableArray alloc] initWithArray: terminals];
2148             [mutableTerminals replaceObjectAtIndex: termIndex withObject: mutableTerm];
2149
2150             [[NSUserDefaults standardUserDefaults] setValue: mutableTerminals forKey: AngbandTerminalsDefaultsKey];
2151         }
2152         [[NSUserDefaults standardUserDefaults] synchronize];
2153     }
2154
2155     term *old = Term;
2156     Term_activate( self->terminal );
2157     Term_resize( self.cols, self.rows );
2158     Term_redraw();
2159     Term_activate( old );
2160 }
2161
2162 - (void)setMinimumWindowSize:(int)termIdx
2163 {
2164     NSSize minsize;
2165
2166     if (termIdx == 0) {
2167         minsize.width = 80;
2168         minsize.height = 24;
2169     } else {
2170         minsize.width = 1;
2171         minsize.height = 1;
2172     }
2173     minsize.width =
2174         minsize.width * self.tileSize.width + self.borderSize.width * 2.0;
2175     minsize.height =
2176         minsize.height * self.tileSize.height + self.borderSize.height * 2.0;
2177     [[self makePrimaryWindow] setContentMinSize:minsize];
2178 }
2179
2180 - (void)saveWindowVisibleToDefaults: (BOOL)windowVisible
2181 {
2182         int termIndex = [self terminalIndex];
2183         BOOL safeVisibility = (termIndex == 0) ? YES : windowVisible; /* Ensure main term doesn't go away because of these defaults */
2184         NSArray *terminals = [[NSUserDefaults standardUserDefaults] valueForKey: AngbandTerminalsDefaultsKey];
2185
2186         if( termIndex < (int)[terminals count] )
2187         {
2188                 NSMutableDictionary *mutableTerm = [[NSMutableDictionary alloc] initWithDictionary: [terminals objectAtIndex: termIndex]];
2189                 [mutableTerm setValue: [NSNumber numberWithBool: safeVisibility] forKey: AngbandTerminalVisibleDefaultsKey];
2190
2191                 NSMutableArray *mutableTerminals = [[NSMutableArray alloc] initWithArray: terminals];
2192                 [mutableTerminals replaceObjectAtIndex: termIndex withObject: mutableTerm];
2193
2194                 [[NSUserDefaults standardUserDefaults] setValue: mutableTerminals forKey: AngbandTerminalsDefaultsKey];
2195         }
2196 }
2197
2198 - (BOOL)windowVisibleUsingDefaults
2199 {
2200         int termIndex = [self terminalIndex];
2201
2202         if( termIndex == 0 )
2203         {
2204                 return YES;
2205         }
2206
2207         NSArray *terminals = [[NSUserDefaults standardUserDefaults] valueForKey: AngbandTerminalsDefaultsKey];
2208         BOOL visible = NO;
2209
2210         if( termIndex < (int)[terminals count] )
2211         {
2212                 NSDictionary *term = [terminals objectAtIndex: termIndex];
2213                 NSNumber *visibleValue = [term valueForKey: AngbandTerminalVisibleDefaultsKey];
2214
2215                 if( visibleValue != nil )
2216                 {
2217                         visible = [visibleValue boolValue];
2218                 }
2219         }
2220
2221         return visible;
2222 }
2223
2224 #pragma mark -
2225 #pragma mark NSWindowDelegate Methods
2226
2227 /*- (void)windowWillStartLiveResize: (NSNotification *)notification
2228
2229 }*/ 
2230
2231 - (void)windowDidEndLiveResize: (NSNotification *)notification
2232 {
2233     NSWindow *window = [notification object];
2234     NSRect contentRect = [window contentRectForFrameRect: [window frame]];
2235     [self resizeTerminalWithContentRect: contentRect saveToDefaults: !(self->inFullscreenTransition)];
2236 }
2237
2238 /*- (NSSize)windowWillResize: (NSWindow *)sender toSize: (NSSize)frameSize
2239 {
2240 } */
2241
2242 - (void)windowWillEnterFullScreen: (NSNotification *)notification
2243 {
2244     self->inFullscreenTransition = YES;
2245 }
2246
2247 - (void)windowDidEnterFullScreen: (NSNotification *)notification
2248 {
2249     NSWindow *window = [notification object];
2250     NSRect contentRect = [window contentRectForFrameRect: [window frame]];
2251     self->inFullscreenTransition = NO;
2252     [self resizeTerminalWithContentRect: contentRect saveToDefaults: NO];
2253 }
2254
2255 - (void)windowWillExitFullScreen: (NSNotification *)notification
2256 {
2257     self->inFullscreenTransition = YES;
2258 }
2259
2260 - (void)windowDidExitFullScreen: (NSNotification *)notification
2261 {
2262     NSWindow *window = [notification object];
2263     NSRect contentRect = [window contentRectForFrameRect: [window frame]];
2264     self->inFullscreenTransition = NO;
2265     [self resizeTerminalWithContentRect: contentRect saveToDefaults: NO];
2266 }
2267
2268 - (void)windowDidBecomeMain:(NSNotification *)notification
2269 {
2270     NSWindow *window = [notification object];
2271
2272     if( window != self.primaryWindow )
2273     {
2274         return;
2275     }
2276
2277     int termIndex = [self terminalIndex];
2278     NSMenuItem *item = [[[NSApplication sharedApplication] windowsMenu] itemWithTag: AngbandWindowMenuItemTagBase + termIndex];
2279     [item setState: NSOnState];
2280
2281     if( [[NSFontPanel sharedFontPanel] isVisible] )
2282     {
2283         [[NSFontPanel sharedFontPanel] setPanelFont:self.angbandViewFont
2284                                        isMultiple: NO];
2285     }
2286 }
2287
2288 - (void)windowDidResignMain: (NSNotification *)notification
2289 {
2290     NSWindow *window = [notification object];
2291
2292     if( window != self.primaryWindow )
2293     {
2294         return;
2295     }
2296
2297     int termIndex = [self terminalIndex];
2298     NSMenuItem *item = [[[NSApplication sharedApplication] windowsMenu] itemWithTag: AngbandWindowMenuItemTagBase + termIndex];
2299     [item setState: NSOffState];
2300 }
2301
2302 - (void)windowWillClose: (NSNotification *)notification
2303 {
2304         [self saveWindowVisibleToDefaults: NO];
2305 }
2306
2307 @end
2308
2309
2310 @implementation AngbandView
2311
2312 - (BOOL)isOpaque
2313 {
2314     return YES;
2315 }
2316
2317 - (BOOL)isFlipped
2318 {
2319     return YES;
2320 }
2321
2322 - (void)drawRect:(NSRect)rect
2323 {
2324     if (! angbandContext)
2325     {
2326         /* Draw bright orange, 'cause this ain't right */
2327         [[NSColor orangeColor] set];
2328         NSRectFill([self bounds]);
2329     }
2330     else
2331     {
2332         /* Tell the Angband context to draw into us */
2333         [angbandContext drawRect:rect inView:self];
2334     }
2335 }
2336
2337 - (void)setAngbandContext:(AngbandContext *)context
2338 {
2339     angbandContext = context;
2340 }
2341
2342 - (AngbandContext *)angbandContext
2343 {
2344     return angbandContext;
2345 }
2346
2347 - (void)setFrameSize:(NSSize)size
2348 {
2349     BOOL changed = ! NSEqualSizes(size, [self frame].size);
2350     [super setFrameSize:size];
2351     if (changed) [angbandContext angbandViewDidScale:self];
2352 }
2353
2354 - (void)viewWillStartLiveResize
2355 {
2356     [angbandContext viewWillStartLiveResize:self];
2357 }
2358
2359 - (void)viewDidEndLiveResize
2360 {
2361     [angbandContext viewDidEndLiveResize:self];
2362 }
2363
2364 @end
2365
2366 /**
2367  * Delay handling of double-clicked savefiles
2368  */
2369 Boolean open_when_ready = FALSE;
2370
2371
2372
2373 /**
2374  * ------------------------------------------------------------------------
2375  * Some generic functions
2376  * ------------------------------------------------------------------------ */
2377
2378 /**
2379  * Sets an Angband color at a given index
2380  */
2381 static void set_color_for_index(int idx)
2382 {
2383     u16b rv, gv, bv;
2384     
2385     /* Extract the R,G,B data */
2386     rv = angband_color_table[idx][1];
2387     gv = angband_color_table[idx][2];
2388     bv = angband_color_table[idx][3];
2389     
2390     CGContextSetRGBFillColor([[NSGraphicsContext currentContext] graphicsPort], rv/255., gv/255., bv/255., 1.);
2391 }
2392
2393 /**
2394  * Remember the current character in UserDefaults so we can select it by
2395  * default next time.
2396  */
2397 static void record_current_savefile(void)
2398 {
2399     NSString *savefileString = [[NSString stringWithCString:savefile encoding:NSMacOSRomanStringEncoding] lastPathComponent];
2400     if (savefileString)
2401     {
2402         NSUserDefaults *angbandDefs = [NSUserDefaults angbandDefaults];
2403         [angbandDefs setObject:savefileString forKey:@"SaveFile"];
2404         [angbandDefs synchronize];
2405     }
2406 }
2407
2408
2409 #ifdef JP
2410 /**
2411  * Convert a two-byte EUC-JP encoded character (both *cp and (*cp + 1) are in
2412  * the range, 0xA1-0xFE, or *cp is 0x8E) to a utf16 value in the native byte
2413  * ordering.
2414  */
2415 static wchar_t convert_two_byte_eucjp_to_utf16_native(const char *cp)
2416 {
2417     NSString* str = [[NSString alloc] initWithBytes:cp length:2
2418                                       encoding:NSJapaneseEUCStringEncoding];
2419     wchar_t result = [str characterAtIndex:0];
2420     str = nil;
2421     return result;
2422 }
2423 #endif /* JP */
2424
2425
2426 /**
2427  * ------------------------------------------------------------------------
2428  * Support for the "z-term.c" package
2429  * ------------------------------------------------------------------------ */
2430
2431
2432 /**
2433  * Initialize a new Term
2434  */
2435 static void Term_init_cocoa(term *t)
2436 {
2437     @autoreleasepool {
2438         AngbandContext *context = [[AngbandContext alloc] init];
2439
2440         /* Give the term ownership of the context */
2441         t->data = (void *)CFBridgingRetain(context);
2442
2443         /* Handle graphics */
2444         t->higher_pict = !! use_graphics;
2445         t->always_pict = FALSE;
2446
2447         NSDisableScreenUpdates();
2448
2449         /*
2450          * Figure out the frame autosave name based on the index of this term
2451          */
2452         NSString *autosaveName = nil;
2453         int termIdx;
2454         for (termIdx = 0; termIdx < ANGBAND_TERM_MAX; termIdx++)
2455         {
2456             if (angband_term[termIdx] == t)
2457             {
2458                 autosaveName =
2459                     [NSString stringWithFormat:@"AngbandTerm-%d", termIdx];
2460                 break;
2461             }
2462         }
2463
2464         /* Set its font. */
2465         NSString *fontName =
2466             [[NSUserDefaults angbandDefaults]
2467                 stringForKey:[NSString stringWithFormat:@"FontName-%d", termIdx]];
2468         if (! fontName) fontName = [[AngbandContext defaultFont] fontName];
2469
2470         /*
2471          * Use a smaller default font for the other windows, but only if the
2472          * font hasn't been explicitly set.
2473          */
2474         float fontSize =
2475             (termIdx > 0) ? 10.0 : [[AngbandContext defaultFont] pointSize];
2476         NSNumber *fontSizeNumber =
2477             [[NSUserDefaults angbandDefaults]
2478                 valueForKey: [NSString stringWithFormat: @"FontSize-%d", termIdx]];
2479
2480         if( fontSizeNumber != nil )
2481         {
2482             fontSize = [fontSizeNumber floatValue];
2483         }
2484
2485         [context setSelectionFont:[NSFont fontWithName:fontName size:fontSize]
2486                  adjustTerminal: NO];
2487
2488         NSArray *terminalDefaults =
2489             [[NSUserDefaults standardUserDefaults]
2490                 valueForKey: AngbandTerminalsDefaultsKey];
2491         NSInteger rows = 24;
2492         NSInteger columns = 80;
2493
2494         if( termIdx < (int)[terminalDefaults count] )
2495         {
2496             NSDictionary *term = [terminalDefaults objectAtIndex: termIdx];
2497             NSInteger defaultRows =
2498                 [[term valueForKey: AngbandTerminalRowsDefaultsKey]
2499                     integerValue];
2500             NSInteger defaultColumns =
2501                 [[term valueForKey: AngbandTerminalColumnsDefaultsKey]
2502                     integerValue];
2503
2504             if (defaultRows > 0) rows = defaultRows;
2505             if (defaultColumns > 0) columns = defaultColumns;
2506         }
2507
2508         context.cols = columns;
2509         context.rows = rows;
2510         [context.changes resize:columns rows:rows];
2511
2512         /* Get the window */
2513         NSWindow *window = [context makePrimaryWindow];
2514
2515         /* Set its title and, for auxiliary terms, tentative size */
2516         NSString *title =
2517             [NSString stringWithCString:angband_term_name[termIdx]
2518 #ifdef JP
2519                       encoding:NSJapaneseEUCStringEncoding
2520 #else
2521                       encoding:NSMacOSRomanStringEncoding
2522 #endif
2523             ];
2524         [window setTitle:title];
2525         [context setMinimumWindowSize:termIdx];
2526
2527         /*
2528          * If this is the first term, and we support full screen (Mac OS X Lion
2529          * or later), then allow it to go full screen (sweet). Allow other
2530          * terms to be FullScreenAuxilliary, so they can at least show up.
2531          * Unfortunately in Lion they don't get brought to the full screen
2532          * space; but they would only make sense on multiple displays anyways
2533          * so it's not a big loss.
2534          */
2535         if ([window respondsToSelector:@selector(toggleFullScreen:)])
2536         {
2537             NSWindowCollectionBehavior behavior = [window collectionBehavior];
2538             behavior |=
2539                 (termIdx == 0 ?
2540                  Angband_NSWindowCollectionBehaviorFullScreenPrimary :
2541                  Angband_NSWindowCollectionBehaviorFullScreenAuxiliary);
2542             [window setCollectionBehavior:behavior];
2543         }
2544
2545         /* No Resume support yet, though it would not be hard to add */
2546         if ([window respondsToSelector:@selector(setRestorable:)])
2547         {
2548             [window setRestorable:NO];
2549         }
2550
2551         /* default window placement */ {
2552             static NSRect overallBoundingRect;
2553
2554             if( termIdx == 0 )
2555             {
2556                 /*
2557                  * This is a bit of a trick to allow us to display multiple
2558                  * windows in the "standard default" window position in OS X:
2559                  * the upper center of the screen.  The term sizes set in
2560                  * AngbandAppDelegate's loadPrefs() are based on a 5-wide by
2561                  * 3-high grid, with the main term being 4/5 wide by 2/3 high
2562                  * (hence the scaling to find what the containing rect would
2563                  * be).
2564                  */
2565                 NSRect originalMainTermFrame = [window frame];
2566                 NSRect scaledFrame = originalMainTermFrame;
2567                 scaledFrame.size.width *= 5.0 / 4.0;
2568                 scaledFrame.size.height *= 3.0 / 2.0;
2569                 scaledFrame.size.width += 1.0; /* spacing between window columns */
2570                 scaledFrame.size.height += 1.0; /* spacing between window rows */
2571                 [window setFrame: scaledFrame  display: NO];
2572                 [window center];
2573                 overallBoundingRect = [window frame];
2574                 [window setFrame: originalMainTermFrame display: NO];
2575             }
2576
2577             static NSRect mainTermBaseRect;
2578             NSRect windowFrame = [window frame];
2579
2580             if( termIdx == 0 )
2581             {
2582                 /*
2583                  * The height and width adjustments were determined
2584                  * experimentally, so that the rest of the windows line up
2585                  * nicely without overlapping.
2586                  */
2587                 windowFrame.size.width += 7.0;
2588                 windowFrame.size.height += 9.0;
2589                 windowFrame.origin.x = NSMinX( overallBoundingRect );
2590                 windowFrame.origin.y =
2591                     NSMaxY( overallBoundingRect ) - NSHeight( windowFrame );
2592                 mainTermBaseRect = windowFrame;
2593             }
2594             else if( termIdx == 1 )
2595             {
2596                 windowFrame.origin.x = NSMinX( mainTermBaseRect );
2597                 windowFrame.origin.y =
2598                     NSMinY( mainTermBaseRect ) - NSHeight( windowFrame ) - 1.0;
2599             }
2600             else if( termIdx == 2 )
2601             {
2602                 windowFrame.origin.x = NSMaxX( mainTermBaseRect ) + 1.0;
2603                 windowFrame.origin.y =
2604                     NSMaxY( mainTermBaseRect ) - NSHeight( windowFrame );
2605             }
2606             else if( termIdx == 3 )
2607             {
2608                 windowFrame.origin.x = NSMaxX( mainTermBaseRect ) + 1.0;
2609                 windowFrame.origin.y =
2610                     NSMinY( mainTermBaseRect ) - NSHeight( windowFrame ) - 1.0;
2611             }
2612             else if( termIdx == 4 )
2613             {
2614                 windowFrame.origin.x = NSMaxX( mainTermBaseRect ) + 1.0;
2615                 windowFrame.origin.y = NSMinY( mainTermBaseRect );
2616             }
2617             else if( termIdx == 5 )
2618             {
2619                 windowFrame.origin.x =
2620                     NSMinX( mainTermBaseRect ) + NSWidth( windowFrame ) + 1.0;
2621                 windowFrame.origin.y =
2622                     NSMinY( mainTermBaseRect ) - NSHeight( windowFrame ) - 1.0;
2623             }
2624
2625             [window setFrame: windowFrame display: NO];
2626         }
2627
2628         /* Override the default frame above if the user has adjusted windows in
2629          * the past */
2630         if (autosaveName) [window setFrameAutosaveName:autosaveName];
2631
2632         /*
2633          * Tell it about its term. Do this after we've sized it so that the
2634          * sizing doesn't trigger redrawing and such.
2635          */
2636         [context setTerm:t];
2637
2638         /*
2639          * Only order front if it's the first term. Other terms will be ordered
2640          * front from AngbandUpdateWindowVisibility(). This is to work around a
2641          * problem where Angband aggressively tells us to initialize terms that
2642          * don't do anything!
2643          */
2644         if (t == angband_term[0])
2645             [context.primaryWindow makeKeyAndOrderFront: nil];
2646
2647         NSEnableScreenUpdates();
2648
2649         /* Set "mapped" flag */
2650         t->mapped_flag = true;
2651     }
2652 }
2653
2654
2655
2656 /**
2657  * Nuke an old Term
2658  */
2659 static void Term_nuke_cocoa(term *t)
2660 {
2661     @autoreleasepool {
2662         AngbandContext *context = (__bridge AngbandContext*) (t->data);
2663         if (context)
2664         {
2665             /* Tell the context to get rid of its windows, etc. */
2666             [context dispose];
2667
2668             /* Balance our CFBridgingRetain from when we created it */
2669             CFRelease(t->data);
2670
2671             /* Done with it */
2672             t->data = NULL;
2673         }
2674     }
2675 }
2676
2677 /**
2678  * Returns the CGImageRef corresponding to an image with the given name in the
2679  * resource directory, transferring ownership to the caller
2680  */
2681 static CGImageRef create_angband_image(NSString *path)
2682 {
2683     CGImageRef decodedImage = NULL, result = NULL;
2684     
2685     /* Try using ImageIO to load the image */
2686     if (path)
2687     {
2688         NSURL *url = [[NSURL alloc] initFileURLWithPath:path isDirectory:NO];
2689         if (url)
2690         {
2691             NSDictionary *options = [[NSDictionary alloc] initWithObjectsAndKeys:(id)kCFBooleanTrue, kCGImageSourceShouldCache, nil];
2692             CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)url, (CFDictionaryRef)options);
2693             if (source)
2694             {
2695                 /* We really want the largest image, but in practice there's
2696                                  * only going to be one */
2697                 decodedImage = CGImageSourceCreateImageAtIndex(source, 0, (CFDictionaryRef)options);
2698                 CFRelease(source);
2699             }
2700         }
2701     }
2702     
2703     /* Draw the sucker to defeat ImageIO's weird desire to cache and decode on
2704          * demand. Our images aren't that big! */
2705     if (decodedImage)
2706     {
2707         size_t width = CGImageGetWidth(decodedImage), height = CGImageGetHeight(decodedImage);
2708         
2709         /* Compute our own bitmap info */
2710         CGBitmapInfo imageBitmapInfo = CGImageGetBitmapInfo(decodedImage);
2711         CGBitmapInfo contextBitmapInfo = kCGBitmapByteOrderDefault;
2712         
2713         switch (imageBitmapInfo & kCGBitmapAlphaInfoMask) {
2714             case kCGImageAlphaNone:
2715             case kCGImageAlphaNoneSkipLast:
2716             case kCGImageAlphaNoneSkipFirst:
2717                 /* No alpha */
2718                 contextBitmapInfo |= kCGImageAlphaNone;
2719                 break;
2720             default:
2721                 /* Some alpha, use premultiplied last which is most efficient. */
2722                 contextBitmapInfo |= kCGImageAlphaPremultipliedLast;
2723                 break;
2724         }
2725
2726         /* Draw the source image flipped, since the view is flipped */
2727         CGContextRef ctx = CGBitmapContextCreate(NULL, width, height, CGImageGetBitsPerComponent(decodedImage), CGImageGetBytesPerRow(decodedImage), CGImageGetColorSpace(decodedImage), contextBitmapInfo);
2728         if (ctx) {
2729             CGContextSetBlendMode(ctx, kCGBlendModeCopy);
2730             CGContextTranslateCTM(ctx, 0.0, height);
2731             CGContextScaleCTM(ctx, 1.0, -1.0);
2732             CGContextDrawImage(
2733                 ctx, CGRectMake(0, 0, width, height), decodedImage);
2734             result = CGBitmapContextCreateImage(ctx);
2735             CFRelease(ctx);
2736         }
2737
2738         CGImageRelease(decodedImage);
2739     }
2740     return result;
2741 }
2742
2743 /**
2744  * React to changes
2745  */
2746 static errr Term_xtra_cocoa_react(void)
2747 {
2748     /* Don't actually switch graphics until the game is running */
2749     if (!initialized || !game_in_progress) return (-1);
2750
2751     @autoreleasepool {
2752         AngbandContext *angbandContext =
2753             (__bridge AngbandContext*) (Term->data);
2754
2755         /* Handle graphics */
2756         int expected_graf_mode = (current_graphics_mode) ?
2757             current_graphics_mode->grafID : GRAPHICS_NONE;
2758         if (graf_mode_req != expected_graf_mode)
2759         {
2760             graphics_mode *new_mode;
2761             if (graf_mode_req != GRAPHICS_NONE) {
2762                 new_mode = get_graphics_mode(graf_mode_req);
2763             } else {
2764                 new_mode = NULL;
2765             }
2766
2767             /* Get rid of the old image. CGImageRelease is NULL-safe. */
2768             CGImageRelease(pict_image);
2769             pict_image = NULL;
2770
2771             /* Try creating the image if we want one */
2772             if (new_mode != NULL)
2773             {
2774                 NSString *img_path =
2775                     [NSString stringWithFormat:@"%s/%s", new_mode->path, new_mode->file];
2776                 pict_image = create_angband_image(img_path);
2777
2778                 /* If we failed to create the image, revert to ASCII. */
2779                 if (! pict_image) {
2780                     new_mode = NULL;
2781                     if (use_bigtile) {
2782                         arg_bigtile = FALSE;
2783                     }
2784                     [[NSUserDefaults angbandDefaults]
2785                         setInteger:GRAPHICS_NONE
2786                         forKey:AngbandGraphicsDefaultsKey];
2787                     [[NSUserDefaults angbandDefaults] synchronize];
2788
2789                     NSString *msg = NSLocalizedStringWithDefaultValue(
2790                         @"Error.TileSetLoadFailed",
2791                         AngbandMessageCatalog,
2792                         [NSBundle mainBundle],
2793                         @"Failed to Load Tile Set",
2794                         @"Alert text for failed tile set load");
2795                     NSString *info = NSLocalizedStringWithDefaultValue(
2796                         @"Error.TileSetRevertToASCII",
2797                         AngbandMessageCatalog,
2798                         [NSBundle mainBundle],
2799                         @"Could not load the tile set.  Switched back to ASCII.",
2800                         @"Alert informative message for failed tile set load");
2801                     NSAlert *alert = [[NSAlert alloc] init];
2802
2803                     alert.messageText = msg;
2804                     alert.informativeText = info;
2805                     NSModalResponse result = [alert runModal];
2806                 }
2807             }
2808
2809             /* Record what we did */
2810             use_graphics = new_mode ? new_mode->grafID : 0;
2811             ANGBAND_GRAF = (new_mode ? new_mode->graf : "ascii");
2812             current_graphics_mode = new_mode;
2813
2814             /*
2815              * Enable or disable higher picts. Note: this should be done for
2816              * all terms.
2817              */
2818             angbandContext->terminal->higher_pict = !! use_graphics;
2819
2820             if (pict_image && current_graphics_mode)
2821             {
2822                 /*
2823                  * Compute the row and column count via the image height and
2824                  * width.
2825                  */
2826                 pict_rows = (int)(CGImageGetHeight(pict_image) /
2827                                   current_graphics_mode->cell_height);
2828                 pict_cols = (int)(CGImageGetWidth(pict_image) /
2829                                   current_graphics_mode->cell_width);
2830             }
2831             else
2832             {
2833                 pict_rows = 0;
2834                 pict_cols = 0;
2835             }
2836
2837             /* Reset visuals */
2838             if (arg_bigtile == use_bigtile)
2839             {
2840                 reset_visuals();
2841             }
2842         }
2843
2844         if (arg_bigtile != use_bigtile) {
2845             /* Reset visuals */
2846             reset_visuals();
2847
2848             Term_activate(angband_term[0]);
2849             Term_resize(angband_term[0]->wid, angband_term[0]->hgt);
2850         }
2851     }
2852
2853     /* Success */
2854     return (0);
2855 }
2856
2857
2858 /**
2859  * Draws one tile as a helper function for Term_xtra_cocoa_fresh().
2860  */
2861 static void draw_image_tile(
2862     NSGraphicsContext* nsContext,
2863     CGContextRef cgContext,
2864     CGImageRef image,
2865     NSRect srcRect,
2866     NSRect dstRect,
2867     NSCompositingOperation op)
2868 {
2869     /* Flip the source rect since the source image is flipped */
2870     CGAffineTransform flip = CGAffineTransformIdentity;
2871     flip = CGAffineTransformTranslate(flip, 0.0, CGImageGetHeight(image));
2872     flip = CGAffineTransformScale(flip, 1.0, -1.0);
2873     CGRect flippedSourceRect =
2874         CGRectApplyAffineTransform(NSRectToCGRect(srcRect), flip);
2875
2876     /*
2877      * When we use high-quality resampling to draw a tile, pixels from outside
2878      * the tile may bleed in, causing graphics artifacts. Work around that.
2879      */
2880     CGImageRef subimage =
2881         CGImageCreateWithImageInRect(image, flippedSourceRect);
2882     [nsContext setCompositingOperation:op];
2883     CGContextDrawImage(cgContext, NSRectToCGRect(dstRect), subimage);
2884     CGImageRelease(subimage);
2885 }
2886
2887
2888 /**
2889  * This is a helper function for Term_xtra_cocoa_fresh():  look before a block
2890  * of text on a row to see if the bounds for rendering and clipping need to be
2891  * extended.
2892  */
2893 static void query_before_text(
2894     PendingTermChanges *tc, int iy, int npre, int* pclip, int* prend)
2895 {
2896     int start = *prend;
2897     int i = start - 1;
2898
2899     while (1) {
2900         if (i < 0 || i < start - npre) {
2901             break;
2902         }
2903         enum PendingCellChangeType ctype = [tc getCellChangeType:i row:iy];
2904
2905         if (ctype == CELL_CHANGE_TILE) {
2906             /*
2907              * The cell has been rendered with a tile.  Do not want to modify
2908              * its contents so the clipping and rendering region can not be
2909              * extended.
2910              */
2911             break;
2912         } else if (ctype == CELL_CHANGE_NONE) {
2913             /*
2914              * It has not changed (or using big tile mode and it is within
2915              * a changed tile but is not the left cell for that tile) so
2916              * inquire what it is.
2917              */
2918             TERM_COLOR a[2];
2919             char c[2];
2920
2921             Term_what(i, iy, a + 1, c + 1);
2922             if (use_graphics && (a[1] & 0x80) && (c[1] & 0x80)) {
2923                 /*
2924                  * It is an unchanged location rendered with a tile.  Do not
2925                  * want to modify its contents so the clipping and rendering
2926                  * region can not be extended.
2927                  */
2928                 break;
2929             }
2930             if (use_bigtile && i > 0) {
2931                 Term_what(i - 1, iy, a, c);
2932                 if (use_graphics && (a[0] & 0x80) && (c[0] & 0x80)) {
2933                     /*
2934                      * It is the right cell of a location rendered with a tile.
2935                      * Do not want to modify its contents so the clipping and
2936                      * rendering region can not be exteded.
2937                      */
2938                     break;
2939                 }
2940             }
2941             /*
2942              * It is unchanged text.  A character from the changed region
2943              * may have extended into it so render it to clear that.
2944              */
2945 #ifdef JP
2946             /* Check to see if it is the second part of a kanji character. */
2947             if (i > 0) {
2948                 Term_what(i - 1, iy, a, c);
2949                 if (iskanji(c)) {
2950                     [tc markTextChange:i-1 row:iy
2951                         glyph:convert_two_byte_eucjp_to_utf16_native(c)
2952                         color:a[0] isDoubleWidth:YES];
2953                     *pclip = i - 1;
2954                     *prend = i - 1;
2955                     --i;
2956                 } else {
2957                     [tc markTextChange:i row:iy
2958                         glyph:c[1] color:a[1] isDoubleWidth:NO];
2959                     *pclip = i;
2960                     *prend = i;
2961                 }
2962             } else {
2963                 [tc markTextChange:i row:iy
2964                     glyph:c[1] color:a[1] isDoubleWidth:NO];
2965                 *pclip = i;
2966                 *prend = i;
2967             }
2968 #else
2969             [tc markTextChange:i row:iy
2970                 glyph:c[1] color:a[1] isDoubleWidth:NO];
2971             *pclip = i;
2972             *prend = i;
2973 #endif
2974             --i;
2975         } else {
2976             /*
2977              * The cell has been wiped or had changed text rendered.  Do
2978              * not need to render.  Can extend the clipping rectangle into it.
2979              */
2980             *pclip = i;
2981             --i;
2982         }
2983     }
2984 }
2985
2986
2987 /**
2988  * This is a helper function for Term_xtra_cocoa_fresh():  look after a block
2989  * of text on a row to see if the bounds for rendering and clipping need to be
2990  * extended.
2991  */
2992 static void query_after_text(
2993     PendingTermChanges *tc, int iy, int npost, int* pclip, int* prend)
2994 {
2995     int end = *prend;
2996     int i = end + 1;
2997     int ncol = tc.columnCount;
2998
2999     while (1) {
3000         if (i >= ncol) {
3001             break;
3002         }
3003
3004         enum PendingCellChangeType ctype = [tc getCellChangeType:i row:iy];
3005
3006         /*
3007          * Be willing to consolidate this block with the one after it.  This
3008          * logic should be sufficient to avoid redraws of the region between
3009          * changed blocks of text if angbandContext.nColPre is zero or one.
3010          * For larger values of nColPre, would need to do something more to
3011          * avoid extra redraws.
3012          */
3013         if (i > end + npost && ctype != CELL_CHANGE_TEXT &&
3014             ctype != CELL_CHANGE_WIPE) {
3015             break;
3016         }
3017
3018         if (ctype == CELL_CHANGE_TILE) {
3019             /*
3020              * The cell has been rendered with a tile.  Do not want to modify
3021              * its contents so the clipping and rendering region can not be
3022              * extended.
3023              */
3024             break;
3025         } else if (ctype == CELL_CHANGE_NONE) {
3026             /* It has not changed so inquire what it is. */
3027             TERM_COLOR a[2];
3028             char c[2];
3029
3030             Term_what(i, iy, a, c);
3031             if (use_graphics && (a[0] & 0x80) && (c[0] & 0x80)) {
3032                 /*
3033                  * It is an unchanged location rendered with a tile.  Do not
3034                  * want to modify its contents so the clipping and rendering
3035                  * region can not be extended.
3036                  */
3037                 break;
3038             }
3039             /*
3040              * It is unchanged text.  A character from the changed region
3041              * may have extended into it so render it to clear that.
3042              */
3043 #ifdef JP
3044             /* Check to see if it is the first part of a kanji character. */
3045             if (i < ncol - 1) {
3046                 Term_what(i + 1, iy, a + 1, c + 1);
3047                 if (iskanji(c)) {
3048                     [tc markTextChange:i row:iy
3049                         glyph:convert_two_byte_eucjp_to_utf16_native(c)
3050                         color:a[0] isDoubleWidth:YES];
3051                     *pclip = i + 1;
3052                     *prend = i + 1;
3053                     ++i;
3054                 } else {
3055                     [tc markTextChange:i row:iy
3056                         glyph:c[0] color:a[0] isDoubleWidth:NO];
3057                     *pclip = i;
3058                     *prend = i;
3059                 }
3060             } else {
3061                 [tc markTextChange:i row:iy
3062                     glyph:c[0] color:a[0] isDoubleWidth:NO];
3063                 *pclip = i;
3064                 *prend = i;
3065             }
3066 #else
3067             [tc markTextChange:i row:iy
3068                 glyph:c[0] color:a[0] isDoubleWidth:NO];
3069             *pclip = i;
3070             *prend = i;
3071 #endif
3072             ++i;
3073         } else {
3074             /*
3075              * Have come to another region of changed text or another region
3076              * to wipe.  Combine the regions to minimize redraws.
3077              */
3078             *pclip = i;
3079             *prend = i;
3080             end = i;
3081             ++i;
3082         }
3083     }
3084 }
3085
3086
3087 /**
3088  * Draw the pending changes saved in angbandContext->changes.
3089  */
3090 static void Term_xtra_cocoa_fresh(AngbandContext* angbandContext)
3091 {
3092     int graf_width, graf_height, alphablend;
3093
3094     if (angbandContext.changes.hasTileChanges) {
3095         CGImageAlphaInfo ainfo = CGImageGetAlphaInfo(pict_image);
3096
3097         graf_width = current_graphics_mode->cell_width;
3098         graf_height = current_graphics_mode->cell_height;
3099         /*
3100          * As of this writing, a value of zero for
3101          * current_graphics_mode->alphablend can mean either that the tile set
3102          * doesn't have an alpha channel or it does but it only takes on values
3103          * of 0 or 255.  For main-cocoa.m's purposes, the latter is rendered
3104          * using the same procedure as if alphablend was nonzero.  The former
3105          * is handled differently, but alphablend doesn't distinguish it from
3106          * the latter.  So ignore alphablend and directly test whether an
3107          * alpha channel is present.
3108          */
3109         alphablend = (ainfo & (kCGImageAlphaPremultipliedFirst |
3110                                kCGImageAlphaPremultipliedLast)) ? 1 : 0;
3111     } else {
3112         graf_width = 0;
3113         graf_height = 0;
3114         alphablend = 0;
3115     }
3116
3117     CGContextRef ctx = [angbandContext lockFocus];
3118
3119     if (angbandContext.changes.hasTextChanges ||
3120         angbandContext.changes.hasWipeChanges) {
3121         NSFont *selectionFont = [angbandContext.angbandViewFont screenFont];
3122         [selectionFont set];
3123     }
3124
3125     for (int iy = angbandContext.changes.firstChangedRow;
3126          iy <= angbandContext.changes.lastChangedRow;
3127          ++iy) {
3128         /* Skip untouched rows. */
3129         if ([angbandContext.changes getFirstChangedColumnInRow:iy] >
3130             [angbandContext.changes getLastChangedColumnInRow:iy]) {
3131             continue;
3132         }
3133         int ix = [angbandContext.changes getFirstChangedColumnInRow:iy];
3134         int ixmax = [angbandContext.changes getLastChangedColumnInRow:iy];
3135
3136         while (1) {
3137             int jx;
3138
3139             if (ix > ixmax) {
3140                 break;
3141             }
3142
3143             switch ([angbandContext.changes getCellChangeType:ix row:iy]) {
3144             case CELL_CHANGE_NONE:
3145                 ++ix;
3146                 break;
3147
3148             case CELL_CHANGE_TILE:
3149                 {
3150                     /*
3151                      * Because changes are made to the compositing mode, save
3152                      * the incoming value.
3153                      */
3154                     NSGraphicsContext *nsContext =
3155                         [NSGraphicsContext currentContext];
3156                     NSCompositingOperation op = nsContext.compositingOperation;
3157                     int step = (use_bigtile) ? 2 : 1;
3158
3159                     jx = ix;
3160                     while (jx <= ixmax &&
3161                            [angbandContext.changes getCellChangeType:jx row:iy]
3162                            == CELL_CHANGE_TILE) {
3163                         NSRect destinationRect =
3164                             [angbandContext rectInImageForTileAtX:jx Y:iy];
3165                         struct PendingTileChange tileIndices =
3166                             [angbandContext.changes
3167                                            getCellTileChange:jx row:iy];
3168                         NSRect sourceRect, terrainRect;
3169
3170                         destinationRect.size.width *= step;
3171                         sourceRect.origin.x = graf_width * tileIndices.fgdCol;
3172                         sourceRect.origin.y = graf_height * tileIndices.fgdRow;
3173                         sourceRect.size.width = graf_width;
3174                         sourceRect.size.height = graf_height;
3175                         terrainRect.origin.x = graf_width * tileIndices.bckCol;
3176                         terrainRect.origin.y = graf_height *
3177                             tileIndices.bckRow;
3178                         terrainRect.size.width = graf_width;
3179                         terrainRect.size.height = graf_height;
3180                         if (alphablend) {
3181                             draw_image_tile(
3182                                 nsContext,
3183                                 ctx,
3184                                 pict_image,
3185                                 terrainRect,
3186                                 destinationRect,
3187                                 NSCompositeCopy);
3188                             /*
3189                              * Skip drawing the foreground if it is the same
3190                              * as the background.
3191                              */
3192                             if (sourceRect.origin.x != terrainRect.origin.x ||
3193                                 sourceRect.origin.y != terrainRect.origin.y) {
3194                                 draw_image_tile(
3195                                     nsContext,
3196                                     ctx,
3197                                     pict_image,
3198                                     sourceRect,
3199                                     destinationRect,
3200                                     NSCompositeSourceOver);
3201                             }
3202                         } else {
3203                             draw_image_tile(
3204                                 nsContext,
3205                                 ctx,
3206                                 pict_image,
3207                                 sourceRect,
3208                                 destinationRect,
3209                                 NSCompositeCopy);
3210                         }
3211                         jx += step;
3212                     }
3213
3214                     [nsContext setCompositingOperation:op];
3215
3216                     NSRect rect =
3217                         [angbandContext rectInImageForTileAtX:ix Y:iy];
3218                     rect.size.width =
3219                         angbandContext.tileSize.width * (jx - ix);
3220                     [angbandContext setNeedsDisplayInBaseRect:rect];
3221                 }
3222                 ix = jx;
3223                 break;
3224
3225             case CELL_CHANGE_WIPE:
3226             case CELL_CHANGE_TEXT:
3227                 /*
3228                  * For a wiped region, treat it as if it had text (the only
3229                  * loss if it was not is some extra work rendering
3230                  * neighboring unchanged text).
3231                  */
3232                 jx = ix + 1;
3233                 while (1) {
3234                     if (jx >= angbandContext.cols) {
3235                         break;
3236                     }
3237                     enum PendingCellChangeType ctype =
3238                         [angbandContext.changes getCellChangeType:jx row:iy];
3239                     if (ctype != CELL_CHANGE_TEXT &&
3240                         ctype != CELL_CHANGE_WIPE) {
3241                         break;
3242                     }
3243                     ++jx;
3244                 }
3245                 {
3246                     int isclip = ix;
3247                     int ieclip = jx - 1;
3248                     int isrend = ix;
3249                     int ierend = jx - 1;
3250                     int set_color = 1;
3251                     TERM_COLOR alast = 0;
3252                     NSRect r;
3253                     int k;
3254
3255                     query_before_text(
3256                         angbandContext.changes,
3257                         iy,
3258                         angbandContext.nColPre,
3259                         &isclip,
3260                         &isrend);
3261                     query_after_text(
3262                         angbandContext.changes,
3263                         iy,
3264                         angbandContext.nColPost,
3265                         &ieclip,
3266                         &ierend
3267                     );
3268                     ix = ierend + 1;
3269
3270                     /* Save the state since the clipping will be modified. */
3271                     CGContextSaveGState(ctx);
3272
3273                     /* Clear the area where rendering will be done. */
3274                     r = [angbandContext rectInImageForTileAtX:isrend Y:iy];
3275                     r.size.width = angbandContext.tileSize.width *
3276                         (ierend - isrend + 1);
3277                     [[NSColor blackColor] set];
3278                     NSRectFill(r);
3279
3280                     /*
3281                      * Clear the current path so it does not affect clipping.
3282                      * Then set the clipping rectangle.  Using
3283                      * CGContextSetTextDrawingMode() to include clipping does
3284                      * not appear to be necessary on 10.14 and is actually
3285                      * detrimental:  when displaying more than one character,
3286                      * only the first is visible.
3287                      */
3288                     CGContextBeginPath(ctx);
3289                     r = [angbandContext rectInImageForTileAtX:isclip Y:iy];
3290                     r.size.width = angbandContext.tileSize.width *
3291                         (ieclip - isclip + 1);
3292                     CGContextClipToRect(ctx, r);
3293
3294                     /* Render. */
3295                     k = isrend;
3296                     while (k <= ierend) {
3297                         if ([angbandContext.changes getCellChangeType:k row:iy]
3298                             == CELL_CHANGE_WIPE) {
3299                             /* Skip over since no rendering is necessary. */
3300                             ++k;
3301                             continue;
3302                         }
3303
3304                         struct PendingTextChange textChange =
3305                             [angbandContext.changes getCellTextChange:k
3306                                            row:iy];
3307                         int anew = textChange.color % MAX_COLORS;
3308                         if (set_color || alast != anew) {
3309                             set_color = 0;
3310                             alast = anew;
3311                             set_color_for_index(anew);
3312                         }
3313
3314                         NSRect rectToDraw =
3315                             [angbandContext rectInImageForTileAtX:k Y:iy];
3316                         if (textChange.doubleWidth) {
3317                             rectToDraw.size.width *= 2.0;
3318                             [angbandContext drawWChar:textChange.glyph
3319                                             inRect:rectToDraw context:ctx];
3320                             k += 2;
3321                         } else {
3322                             [angbandContext drawWChar:textChange.glyph
3323                                             inRect:rectToDraw context:ctx];
3324                             ++k;
3325                         }
3326                     }
3327
3328                     /*
3329                      * Inform the context that the area in the clipping
3330                      * rectangle needs to be redisplayed.
3331                      */
3332                     [angbandContext setNeedsDisplayInBaseRect:r];
3333
3334                     CGContextRestoreGState(ctx);
3335                 }
3336                 break;
3337             }
3338         }
3339     }
3340
3341     if (angbandContext.changes.cursorColumn >= 0 &&
3342         angbandContext.changes.cursorRow >= 0) {
3343         NSRect rect = [angbandContext
3344                           rectInImageForTileAtX:angbandContext.changes.cursorColumn
3345                           Y:angbandContext.changes.cursorRow];
3346
3347         rect.size.width *= angbandContext.changes.cursorWidth;
3348         rect.size.height *= angbandContext.changes.cursorHeight;
3349         [[NSColor yellowColor] set];
3350         NSFrameRectWithWidth(rect, 1);
3351         /* Invalidate that rect */
3352         [angbandContext setNeedsDisplayInBaseRect:rect];
3353     }
3354
3355     [angbandContext unlockFocus];
3356 }
3357
3358
3359 /**
3360  * Do a "special thing"
3361  */
3362 static errr Term_xtra_cocoa(int n, int v)
3363 {
3364     errr result = 0;
3365     @autoreleasepool {
3366         AngbandContext* angbandContext =
3367             (__bridge AngbandContext*) (Term->data);
3368
3369         /* Analyze */
3370         switch (n) {
3371             /* Make a noise */
3372         case TERM_XTRA_NOISE:
3373             NSBeep();
3374             break;
3375
3376             /*  Make a sound */
3377         case TERM_XTRA_SOUND:
3378             play_sound(v);
3379             break;
3380
3381             /* Process random events */
3382         case TERM_XTRA_BORED:
3383             /*
3384              * Show or hide cocoa windows based on the subwindow flags set by
3385              * the user.
3386              */
3387             AngbandUpdateWindowVisibility();
3388             /* Process an event */
3389             (void)check_events(CHECK_EVENTS_NO_WAIT);
3390             break;
3391
3392             /* Process pending events */
3393         case TERM_XTRA_EVENT:
3394             /* Process an event */
3395             (void)check_events(v);
3396             break;
3397
3398             /* Flush all pending events (if any) */
3399         case TERM_XTRA_FLUSH:
3400             /* Hack -- flush all events */
3401             while (check_events(CHECK_EVENTS_DRAIN)) /* loop */;
3402
3403             break;
3404
3405             /* Hack -- Change the "soft level" */
3406         case TERM_XTRA_LEVEL:
3407             /*
3408              * Here we could activate (if requested), but I don't think
3409              * Angband should be telling us our window order (the user
3410              * should decide that), so do nothing.
3411              */
3412             break;
3413
3414             /* Clear the screen */
3415         case TERM_XTRA_CLEAR:
3416             {
3417                 [angbandContext lockFocus];
3418                 [[NSColor blackColor] set];
3419                 NSRect imageRect = {NSZeroPoint, [angbandContext imageSize]};
3420                 NSRectFillUsingOperation(imageRect, NSCompositeCopy);
3421                 [angbandContext unlockFocus];
3422                 [angbandContext setNeedsDisplay:YES];
3423                 /* Success */
3424                 break;
3425             }
3426
3427             /* React to changes */
3428         case TERM_XTRA_REACT:
3429             result = Term_xtra_cocoa_react();
3430             break;
3431
3432             /* Delay (milliseconds) */
3433         case TERM_XTRA_DELAY:
3434             /* If needed */
3435             if (v > 0) {
3436                 double seconds = v / 1000.;
3437                 NSDate* date = [NSDate dateWithTimeIntervalSinceNow:seconds];
3438                 do {
3439                     NSEvent* event;
3440                     do {
3441                         event = [NSApp nextEventMatchingMask:-1
3442                                        untilDate:date
3443                                        inMode:NSDefaultRunLoopMode
3444                                        dequeue:YES];
3445                         if (event) send_event(event);
3446                     } while (event);
3447                 } while ([date timeIntervalSinceNow] >= 0);
3448             }
3449             break;
3450
3451             /* Draw the pending changes. */
3452         case TERM_XTRA_FRESH:
3453             Term_xtra_cocoa_fresh(angbandContext);
3454             [angbandContext.changes clear];
3455             break;
3456
3457         default:
3458             /* Oops */
3459             result = 1;
3460             break;
3461         }
3462     }
3463
3464     return result;
3465 }
3466
3467 static errr Term_curs_cocoa(TERM_LEN x, TERM_LEN y)
3468 {
3469     AngbandContext *angbandContext = (__bridge AngbandContext*) (Term->data);
3470
3471     [angbandContext.changes markCursor:x row:y];
3472
3473     /* Success */
3474     return 0;
3475 }
3476
3477 /**
3478  * Draw a cursor that's two tiles wide.  For Japanese, that's used when
3479  * the cursor points at a kanji character, irregardless of whether operating
3480  * in big tile mode.
3481  */
3482 static errr Term_bigcurs_cocoa(TERM_LEN x, TERM_LEN y)
3483 {
3484     AngbandContext *angbandContext = (__bridge AngbandContext*) (Term->data);
3485
3486     [angbandContext.changes markBigCursor:x row:y cellsWide:2 cellsHigh:1];
3487
3488     /* Success */
3489     return 0;
3490 }
3491
3492 /**
3493  * Low level graphics (Assumes valid input)
3494  *
3495  * Erase "n" characters starting at (x,y)
3496  */
3497 static errr Term_wipe_cocoa(TERM_LEN x, TERM_LEN y, int n)
3498 {
3499     AngbandContext *angbandContext = (__bridge AngbandContext*) (Term->data);
3500
3501     [angbandContext.changes markWipeRange:x row:y n:n];
3502
3503     /* Success */
3504     return 0;
3505 }
3506
3507 static errr Term_pict_cocoa(TERM_LEN x, TERM_LEN y, int n,
3508                             TERM_COLOR *ap, concptr cp,
3509                             const TERM_COLOR *tap, concptr tcp)
3510 {
3511     /* Paranoia: Bail if we don't have a current graphics mode */
3512     if (! current_graphics_mode) return -1;
3513
3514     AngbandContext* angbandContext = (__bridge AngbandContext*) (Term->data);
3515     int step = (use_bigtile) ? 2 : 1;
3516
3517     /*
3518      * In bigtile mode, it is sufficient that the bounds for the modified
3519      * region only encompass the left cell for the region affected by the
3520      * tile and that only that cell has to have the details of the changes.
3521      */
3522     for (int i = x; i < x + n * step; i += step) {
3523         TERM_COLOR a = *ap++;
3524         char c = *cp++;
3525         TERM_COLOR ta = *tap++;
3526         char tc = *tcp++;
3527
3528         if (use_graphics && (a & 0x80) && (c & 0x80)) {
3529             [angbandContext.changes markTileChange:i row:y
3530                            foregroundCol:((byte)c & 0x7F) % pict_cols
3531                            foregroundRow:((byte)a & 0x7F) % pict_rows
3532                            backgroundCol:((byte)tc & 0x7F) % pict_cols
3533                            backgroundRow:((byte)ta & 0x7F) % pict_rows];
3534         }
3535     }
3536
3537     /* Success */
3538     return (0);
3539 }
3540
3541 /**
3542  * Low level graphics.  Assumes valid input.
3543  *
3544  * Draw several ("n") chars, with an attr, at a given location.
3545  */
3546 static errr Term_text_cocoa(
3547     TERM_LEN x, TERM_LEN y, int n, TERM_COLOR a, concptr cp)
3548 {
3549     AngbandContext* angbandContext = (__bridge AngbandContext*) (Term->data);
3550     int i = 0;
3551
3552     while (i < n) {
3553 #ifdef JP
3554         if (iskanji(*cp)) {
3555             if (i == n - 1) {
3556                 /*
3557                  * The second byte of the character is past the end.  Ignore
3558                  * the character.
3559                  */
3560                 break;
3561             } else {
3562                 [angbandContext.changes markTextChange:i+x row:y
3563                                glyph:convert_two_byte_eucjp_to_utf16_native(cp)
3564                                color:a isDoubleWidth:YES];
3565                 cp += 2;
3566                 i += 2;
3567             }
3568         } else {
3569             [angbandContext.changes markTextChange:i+x row:y
3570                            glyph:*cp color:a isDoubleWidth:NO];
3571             ++cp;
3572             ++i;
3573         }
3574 #else
3575         [angbandContext.changes markTextChange:i+x row:y
3576                        glyph:*cp color:a isDoubleWidth:NO];
3577         ++cp;
3578         ++i;
3579 #endif
3580     }
3581
3582     /* Success */
3583     return 0;
3584 }
3585
3586 /**
3587  * Post a nonsense event so that our event loop wakes up
3588  */
3589 static void wakeup_event_loop(void)
3590 {
3591     /* Big hack - send a nonsense event to make us update */
3592     NSEvent *event = [NSEvent otherEventWithType:NSApplicationDefined location:NSZeroPoint modifierFlags:0 timestamp:0 windowNumber:0 context:NULL subtype:AngbandEventWakeup data1:0 data2:0];
3593     [NSApp postEvent:event atStart:NO];
3594 }
3595
3596
3597 /**
3598  * Handle the "open_when_ready" flag
3599  */
3600 static void handle_open_when_ready(void)
3601 {
3602     /* Check the flag XXX XXX XXX make a function for this */
3603     if (open_when_ready && initialized && !game_in_progress)
3604     {
3605         /* Forget */
3606         open_when_ready = FALSE;
3607         
3608         /* Game is in progress */
3609         game_in_progress = TRUE;
3610         
3611         /* Wait for a keypress */
3612         pause_line(Term->hgt - 1);
3613     }
3614 }
3615
3616
3617 /**
3618  * Handle quit_when_ready, by Peter Ammon,
3619  * slightly modified to check inkey_flag.
3620  */
3621 static void quit_calmly(void)
3622 {
3623     /* Quit immediately if game's not started */
3624     if (!game_in_progress || !character_generated) quit(NULL);
3625
3626     /* Save the game and Quit (if it's safe) */
3627     if (inkey_flag)
3628     {
3629         /* Hack -- Forget messages and term */
3630         msg_flag = FALSE;
3631                 Term->mapped_flag = FALSE;
3632
3633         /* Save the game */
3634         do_cmd_save_game(FALSE);
3635         record_current_savefile();
3636
3637         /* Quit */
3638         quit(NULL);
3639     }
3640
3641     /* Wait until inkey_flag is set */
3642 }
3643
3644
3645
3646 /**
3647  * Returns YES if we contain an AngbandView (and hence should direct our events
3648  * to Angband)
3649  */
3650 static BOOL contains_angband_view(NSView *view)
3651 {
3652     if ([view isKindOfClass:[AngbandView class]]) return YES;
3653     for (NSView *subview in [view subviews]) {
3654         if (contains_angband_view(subview)) return YES;
3655     }
3656     return NO;
3657 }
3658
3659
3660 /**
3661  * Queue mouse presses if they occur in the map section of the main window.
3662  */
3663 static void AngbandHandleEventMouseDown( NSEvent *event )
3664 {
3665 #if 0
3666         AngbandContext *angbandContext = [[[event window] contentView] angbandContext];
3667         AngbandContext *mainAngbandContext =
3668             (__bridge AngbandContext*) (angband_term[0]->data);
3669
3670         if (mainAngbandContext.primaryWindow &&
3671             [[event window] windowNumber] ==
3672             [mainAngbandContext.primaryWindow windowNumber])
3673         {
3674                 int cols, rows, x, y;
3675                 Term_get_size(&cols, &rows);
3676                 NSSize tileSize = angbandContext.tileSize;
3677                 NSSize border = angbandContext.borderSize;
3678                 NSPoint windowPoint = [event locationInWindow];
3679
3680                 /* Adjust for border; add border height because window origin is at
3681                  * bottom */
3682                 windowPoint = NSMakePoint( windowPoint.x - border.width, windowPoint.y + border.height );
3683
3684                 NSPoint p = [[[event window] contentView] convertPoint: windowPoint fromView: nil];
3685                 x = floor( p.x / tileSize.width );
3686                 y = floor( p.y / tileSize.height );
3687
3688                 /* Being safe about this, since xcode doesn't seem to like the
3689                  * bool_hack stuff */
3690                 BOOL displayingMapInterface = ((int)inkey_flag != 0);
3691
3692                 /* Sidebar plus border == thirteen characters; top row is reserved. */
3693                 /* Coordinates run from (0,0) to (cols-1, rows-1). */
3694                 BOOL mouseInMapSection = (x > 13 && x <= cols - 1 && y > 0  && y <= rows - 2);
3695
3696                 /* If we are displaying a menu, allow clicks anywhere; if we are
3697                  * displaying the main game interface, only allow clicks in the map
3698                  * section */
3699                 if (!displayingMapInterface || (displayingMapInterface && mouseInMapSection))
3700                 {
3701                         /* [event buttonNumber] will return 0 for left click,
3702                          * 1 for right click, but this is safer */
3703                         int button = ([event type] == NSLeftMouseDown) ? 1 : 2;
3704
3705 #ifdef KC_MOD_ALT
3706                         NSUInteger eventModifiers = [event modifierFlags];
3707                         byte angbandModifiers = 0;
3708                         angbandModifiers |= (eventModifiers & NSShiftKeyMask) ? KC_MOD_SHIFT : 0;
3709                         angbandModifiers |= (eventModifiers & NSControlKeyMask) ? KC_MOD_CONTROL : 0;
3710                         angbandModifiers |= (eventModifiers & NSAlternateKeyMask) ? KC_MOD_ALT : 0;
3711                         button |= (angbandModifiers & 0x0F) << 4; /* encode modifiers in the button number (see Term_mousepress()) */
3712 #endif
3713
3714                         Term_mousepress(x, y, button);
3715                 }
3716         }
3717 #endif
3718
3719         /* Pass click through to permit focus change, resize, etc. */
3720         [NSApp sendEvent:event];
3721 }
3722
3723
3724
3725 /**
3726  * Encodes an NSEvent Angband-style, or forwards it along.  Returns YES if the
3727  * event was sent to Angband, NO if Cocoa (or nothing) handled it */
3728 static BOOL send_event(NSEvent *event)
3729 {
3730
3731     /* If the receiving window is not an Angband window, then do nothing */
3732     if (! contains_angband_view([[event window] contentView]))
3733     {
3734         [NSApp sendEvent:event];
3735         return NO;
3736     }
3737
3738     /* Analyze the event */
3739     switch ([event type])
3740     {
3741         case NSKeyDown:
3742         {
3743             /* Try performing a key equivalent */
3744             if ([[NSApp mainMenu] performKeyEquivalent:event]) break;
3745             
3746             unsigned modifiers = [event modifierFlags];
3747             
3748             /* Send all NSCommandKeyMasks through */
3749             if (modifiers & NSCommandKeyMask)
3750             {
3751                 [NSApp sendEvent:event];
3752                 break;
3753             }
3754             
3755             if (! [[event characters] length]) break;
3756             
3757             
3758             /* Extract some modifiers */
3759             int mc = !! (modifiers & NSControlKeyMask);
3760             int ms = !! (modifiers & NSShiftKeyMask);
3761             int mo = !! (modifiers & NSAlternateKeyMask);
3762             int kp = !! (modifiers & NSNumericPadKeyMask);
3763             
3764             
3765             /* Get the Angband char corresponding to this unichar */
3766             unichar c = [[event characters] characterAtIndex:0];
3767             char ch;
3768             /*
3769              * Have anything from the numeric keypad generate a macro
3770              * trigger so that shift or control modifiers can be passed.
3771              */
3772             if (c <= 0x7F && !kp)
3773             {
3774                 ch = (char) c;
3775             }
3776             else {
3777                 /*
3778                  * The rest of Hengband uses Angband 2.7's or so key handling:
3779                  * so for the rest do something like the encoding that
3780                  * main-win.c does:  send a macro trigger with the Unicode
3781                  * value encoded into printable ASCII characters.
3782                  */
3783                 ch = '\0';
3784             }
3785             
3786             /* override special keys */
3787             switch([event keyCode]) {
3788                 case kVK_Return: ch = '\r'; break;
3789                 case kVK_Escape: ch = 27; break;
3790                 case kVK_Tab: ch = '\t'; break;
3791                 case kVK_Delete: ch = '\b'; break;
3792                 case kVK_ANSI_KeypadEnter: ch = '\r'; kp = TRUE; break;
3793             }
3794
3795             /* Hide the mouse pointer */
3796             [NSCursor setHiddenUntilMouseMoves:YES];
3797             
3798             /* Enqueue it */
3799             if (ch != '\0')
3800             {
3801                 Term_keypress(ch);
3802             }
3803             else
3804             {
3805                 /*
3806                  * Could use the hexsym global but some characters overlap with
3807                  * those used to indicate modifiers.
3808                  */
3809                 const char encoded[16] = {
3810                     '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b',
3811                     'c', 'd', 'e', 'f'
3812                 };
3813
3814                 /* Begin the macro trigger. */
3815                 Term_keypress(31);
3816
3817                 /* Send the modifiers. */
3818                 if (mc) Term_keypress('C');
3819                 if (ms) Term_keypress('S');
3820                 if (mo) Term_keypress('O');
3821                 if (kp) Term_keypress('K');
3822
3823                 do {
3824                     Term_keypress(encoded[c & 0xF]);
3825                     c >>= 4;
3826                 } while (c > 0);
3827
3828                 /* End the macro trigger. */
3829                 Term_keypress(13);
3830             }
3831             
3832             break;
3833         }
3834             
3835         case NSLeftMouseDown:
3836                 case NSRightMouseDown:
3837                         AngbandHandleEventMouseDown(event);
3838             break;
3839
3840         case NSApplicationDefined:
3841         {
3842             if ([event subtype] == AngbandEventWakeup)
3843             {
3844                 return YES;
3845             }
3846             break;
3847         }
3848             
3849         default:
3850             [NSApp sendEvent:event];
3851             return YES;
3852     }
3853     return YES;
3854 }
3855
3856 /**
3857  * Check for Events, return TRUE if we process any
3858  */
3859 static BOOL check_events(int wait)
3860 {
3861     BOOL result = YES;
3862
3863     @autoreleasepool {
3864         /* Handles the quit_when_ready flag */
3865         if (quit_when_ready) quit_calmly();
3866
3867         NSDate* endDate;
3868         if (wait == CHECK_EVENTS_WAIT) endDate = [NSDate distantFuture];
3869         else endDate = [NSDate distantPast];
3870
3871         NSEvent* event;
3872         for (;;) {
3873             if (quit_when_ready)
3874             {
3875                 /* send escape events until we quit */
3876                 Term_keypress(0x1B);
3877                 result = NO;
3878                 break;
3879             }
3880             else {
3881                 event = [NSApp nextEventMatchingMask:-1 untilDate:endDate
3882                                inMode:NSDefaultRunLoopMode dequeue:YES];
3883                 if (! event) {
3884                     result = NO;
3885                     break;
3886                 }
3887                 if (send_event(event)) break;
3888             }
3889         }
3890     }
3891
3892     return result;
3893 }
3894
3895 /**
3896  * Hook to tell the user something important
3897  */
3898 static void hook_plog(const char * str)
3899 {
3900     if (str)
3901     {
3902         NSString *msg = NSLocalizedStringWithDefaultValue(
3903             @"Warning", AngbandMessageCatalog, [NSBundle mainBundle],
3904             @"Warning", @"Alert text for generic warning");
3905         NSString *info = [NSString stringWithCString:str
3906 #ifdef JP
3907                                    encoding:NSJapaneseEUCStringEncoding
3908 #else
3909                                    encoding:NSMacOSRomanStringEncoding
3910 #endif
3911         ];
3912         NSAlert *alert = [[NSAlert alloc] init];
3913
3914         alert.messageText = msg;
3915         alert.informativeText = info;
3916         NSModalResponse result = [alert runModal];
3917     }
3918 }
3919
3920
3921 /**
3922  * Hook to tell the user something, and then quit
3923  */
3924 static void hook_quit(const char * str)
3925 {
3926     plog(str);
3927     exit(0);
3928 }
3929
3930 /**
3931  * Return the path for Angband's lib directory and bail if it isn't found. The
3932  * lib directory should be in the bundle's resources directory, since it's
3933  * copied when built.
3934  */
3935 static NSString* get_lib_directory(void)
3936 {
3937     NSString *bundleLibPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent: AngbandDirectoryNameLib];
3938     BOOL isDirectory = NO;
3939     BOOL libExists = [[NSFileManager defaultManager] fileExistsAtPath: bundleLibPath isDirectory: &isDirectory];
3940
3941     if( !libExists || !isDirectory )
3942     {
3943         NSLog( @"Hengband: can't find %@/ in bundle: isDirectory: %d libExists: %d", AngbandDirectoryNameLib, isDirectory, libExists );
3944
3945         NSString *msg = NSLocalizedStringWithDefaultValue(
3946             @"Error.MissingResources",
3947             AngbandMessageCatalog,
3948             [NSBundle mainBundle],
3949             @"Missing Resources",
3950             @"Alert text for missing resources");
3951         NSString *info = NSLocalizedStringWithDefaultValue(
3952             @"Error.MissingAngbandLib",
3953             AngbandMessageCatalog,
3954             [NSBundle mainBundle],
3955             @"Hengband was unable to find required resources and must quit. Please report a bug on the Angband forums.",
3956             @"Alert informative message for missing Angband lib/ folder");
3957         NSString *quit_label = NSLocalizedStringWithDefaultValue(
3958             @"Label.Quit", AngbandMessageCatalog, [NSBundle mainBundle],
3959             @"Quit", @"Quit");
3960         NSAlert *alert = [[NSAlert alloc] init];
3961
3962         /*
3963          * Note that NSCriticalAlertStyle was deprecated in 10.10.  The
3964          * replacement is NSAlertStyleCritical.
3965          */
3966         alert.alertStyle = NSCriticalAlertStyle;
3967         alert.messageText = msg;
3968         alert.informativeText = info;
3969         [alert addButtonWithTitle:quit_label];
3970         NSModalResponse result = [alert runModal];
3971         exit( 0 );
3972     }
3973
3974     return bundleLibPath;
3975 }
3976
3977 /**
3978  * Return the path for the directory where Angband should look for its standard
3979  * user file tree.
3980  */
3981 static NSString* get_doc_directory(void)
3982 {
3983         NSString *documents = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
3984
3985 #if defined(SAFE_DIRECTORY)
3986         NSString *versionedDirectory = [NSString stringWithFormat: @"%@-%s", AngbandDirectoryNameBase, VERSION_STRING];
3987         return [documents stringByAppendingPathComponent: versionedDirectory];
3988 #else
3989         return [documents stringByAppendingPathComponent: AngbandDirectoryNameBase];
3990 #endif
3991 }
3992
3993 /**
3994  * Adjust directory paths as needed to correct for any differences needed by
3995  * Angband.  init_file_paths() currently requires that all paths provided have
3996  * a trailing slash and all other platforms honor this.
3997  *
3998  * \param originalPath The directory path to adjust.
3999  * \return A path suitable for Angband or nil if an error occurred.
4000  */
4001 static NSString* AngbandCorrectedDirectoryPath(NSString *originalPath)
4002 {
4003         if ([originalPath length] == 0) {
4004                 return nil;
4005         }
4006
4007         if (![originalPath hasSuffix: @"/"]) {
4008                 return [originalPath stringByAppendingString: @"/"];
4009         }
4010
4011         return originalPath;
4012 }
4013
4014 /**
4015  * Give Angband the base paths that should be used for the various directories
4016  * it needs. It will create any needed directories.
4017  */
4018 static void prepare_paths_and_directories(void)
4019 {
4020         char libpath[PATH_MAX + 1] = "\0";
4021         NSString *libDirectoryPath =
4022             AngbandCorrectedDirectoryPath(get_lib_directory());
4023         [libDirectoryPath getFileSystemRepresentation: libpath maxLength: sizeof(libpath)];
4024
4025         char basepath[PATH_MAX + 1] = "\0";
4026         NSString *angbandDocumentsPath =
4027             AngbandCorrectedDirectoryPath(get_doc_directory());
4028         [angbandDocumentsPath getFileSystemRepresentation: basepath maxLength: sizeof(basepath)];
4029
4030         init_file_paths(libpath, basepath);
4031         create_needed_dirs();
4032 }
4033
4034 /**
4035  * Play sound effects asynchronously.  Select a sound from any available
4036  * for the required event, and bridge to Cocoa to play it.
4037  */
4038 static void play_sound(int event)
4039 {
4040     [[AngbandSoundCatalog sharedSounds] playSound:event];
4041 }
4042
4043 /**
4044  * ------------------------------------------------------------------------
4045  * Main program
4046  * ------------------------------------------------------------------------ */
4047
4048 @implementation AngbandAppDelegate
4049
4050 @synthesize graphicsMenu=_graphicsMenu;
4051 @synthesize commandMenu=_commandMenu;
4052 @synthesize commandMenuTagMap=_commandMenuTagMap;
4053
4054 - (IBAction)newGame:sender
4055 {
4056     /* Game is in progress */
4057     game_in_progress = TRUE;
4058     new_game = TRUE;
4059 }
4060
4061 - (IBAction)editFont:sender
4062 {
4063     NSFontPanel *panel = [NSFontPanel sharedFontPanel];
4064     NSFont *termFont = [AngbandContext defaultFont];
4065
4066     int i;
4067     for (i=0; i < ANGBAND_TERM_MAX; i++) {
4068         AngbandContext *context =
4069             (__bridge AngbandContext*) (angband_term[i]->data);
4070         if ([context isMainWindow]) {
4071             termFont = [context angbandViewFont];
4072             break;
4073         }
4074     }
4075
4076     [panel setPanelFont:termFont isMultiple:NO];
4077     [panel orderFront:self];
4078 }
4079
4080 /**
4081  * Implent NSObject's changeFont() method to receive a notification about the
4082  * changed font.  Note that, as of 10.14, changeFont() is deprecated in
4083  * NSObject - it will be removed at some point and the application delegate
4084  * will have to be declared as implementing the NSFontChanging protocol.
4085  */
4086 - (void)changeFont:(id)sender
4087 {
4088     int mainTerm;
4089     for (mainTerm=0; mainTerm < ANGBAND_TERM_MAX; mainTerm++) {
4090         AngbandContext *context =
4091             (__bridge AngbandContext*) (angband_term[mainTerm]->data);
4092         if ([context isMainWindow]) {
4093             break;
4094         }
4095     }
4096
4097     /* Bug #1709: Only change font for angband windows */
4098     if (mainTerm == ANGBAND_TERM_MAX) return;
4099
4100     NSFont *oldFont = [AngbandContext defaultFont];
4101     NSFont *newFont = [sender convertFont:oldFont];
4102     if (! newFont) return; /*paranoia */
4103
4104     /* Store as the default font if we changed the first term */
4105     if (mainTerm == 0) {
4106         [AngbandContext setDefaultFont:newFont];
4107     }
4108
4109     /* Record it in the preferences */
4110     NSUserDefaults *defs = [NSUserDefaults angbandDefaults];
4111     [defs setValue:[newFont fontName] 
4112         forKey:[NSString stringWithFormat:@"FontName-%d", mainTerm]];
4113     [defs setFloat:[newFont pointSize]
4114         forKey:[NSString stringWithFormat:@"FontSize-%d", mainTerm]];
4115     [defs synchronize];
4116
4117     NSDisableScreenUpdates();
4118
4119     /* Update window */
4120     AngbandContext *angbandContext =
4121         (__bridge AngbandContext*) (angband_term[mainTerm]->data);
4122     [(id)angbandContext setSelectionFont:newFont adjustTerminal: YES];
4123
4124     NSEnableScreenUpdates();
4125
4126     if (mainTerm == 0 && game_in_progress) {
4127         /* Mimics the logic in setGraphicsMode(). */
4128         do_cmd_redraw();
4129         wakeup_event_loop();
4130     } else {
4131         [(id)angbandContext requestRedraw];
4132     }
4133 }
4134
4135 - (IBAction)openGame:sender
4136 {
4137     @autoreleasepool {
4138         BOOL selectedSomething = NO;
4139         int panelResult;
4140
4141         /* Get where we think the save files are */
4142         NSURL *startingDirectoryURL =
4143             [NSURL fileURLWithPath:[NSString stringWithCString:ANGBAND_DIR_SAVE encoding:NSASCIIStringEncoding]
4144                    isDirectory:YES];
4145
4146         /* Set up an open panel */
4147         NSOpenPanel* panel = [NSOpenPanel openPanel];
4148         [panel setCanChooseFiles:YES];
4149         [panel setCanChooseDirectories:NO];
4150         [panel setResolvesAliases:YES];
4151         [panel setAllowsMultipleSelection:NO];
4152         [panel setTreatsFilePackagesAsDirectories:YES];
4153         [panel setDirectoryURL:startingDirectoryURL];
4154
4155         /* Run it */
4156         panelResult = [panel runModal];
4157         if (panelResult == NSOKButton)
4158         {
4159             NSArray* fileURLs = [panel URLs];
4160             if ([fileURLs count] > 0 && [[fileURLs objectAtIndex:0] isFileURL])
4161             {
4162                 NSURL* savefileURL = (NSURL *)[fileURLs objectAtIndex:0];
4163                 /*
4164                  * The path property doesn't do the right thing except for
4165                  * URLs with the file scheme. We had
4166                  * getFileSystemRepresentation here before, but that wasn't
4167                  * introduced until OS X 10.9.
4168                  */
4169                 selectedSomething = [[savefileURL path]
4170                                         getCString:savefile
4171                                         maxLength:sizeof savefile
4172                                         encoding:NSMacOSRomanStringEncoding];
4173             }
4174         }
4175
4176         if (selectedSomething)
4177         {
4178             /* Remember this so we can select it by default next time */
4179             record_current_savefile();
4180
4181             /* Game is in progress */
4182             game_in_progress = TRUE;
4183             new_game = FALSE;
4184         }
4185     }
4186 }
4187
4188 - (IBAction)saveGame:sender
4189 {
4190     /* Hack -- Forget messages */
4191     msg_flag = FALSE;
4192     
4193     /* Save the game */
4194     do_cmd_save_game(FALSE);
4195     
4196     /* Record the current save file so we can select it by default next time.
4197          * It's a little sketchy that this only happens when we save through the
4198          * menu; ideally game-triggered saves would trigger it too. */
4199     record_current_savefile();
4200 }
4201
4202 /**
4203  * Create and initialize Angband terminal number "termIndex".
4204  */
4205 - (void)linkTermData:(int)termIndex
4206 {
4207     NSArray *terminalDefaults = [[NSUserDefaults standardUserDefaults]
4208                                     valueForKey: AngbandTerminalsDefaultsKey];
4209     NSInteger rows = 24;
4210     NSInteger columns = 80;
4211
4212     if (termIndex < (int)[terminalDefaults count]) {
4213         NSDictionary *term = [terminalDefaults objectAtIndex:termIndex];
4214         rows = [[term valueForKey: AngbandTerminalRowsDefaultsKey]
4215                    integerValue];
4216         columns = [[term valueForKey: AngbandTerminalColumnsDefaultsKey]
4217                       integerValue];
4218     }
4219
4220     /* Allocate */
4221     term *newterm = ZNEW(term);
4222
4223     /* Initialize the term */
4224     term_init(newterm, columns, rows, 256 /* keypresses, for some reason? */);
4225
4226     /* Differentiate between BS/^h, Tab/^i, etc. */
4227     /* newterm->complex_input = TRUE; */
4228
4229     /* Use a "software" cursor */
4230     newterm->soft_cursor = TRUE;
4231
4232     /* Disable the per-row flush notifications since they are not used. */
4233     newterm->never_frosh = TRUE;
4234
4235     /* Erase with "white space" */
4236     newterm->attr_blank = TERM_WHITE;
4237     newterm->char_blank = ' ';
4238
4239     /* Prepare the init/nuke hooks */
4240     newterm->init_hook = Term_init_cocoa;
4241     newterm->nuke_hook = Term_nuke_cocoa;
4242
4243     /* Prepare the function hooks */
4244     newterm->xtra_hook = Term_xtra_cocoa;
4245     newterm->wipe_hook = Term_wipe_cocoa;
4246     newterm->curs_hook = Term_curs_cocoa;
4247     newterm->bigcurs_hook = Term_bigcurs_cocoa;
4248     newterm->text_hook = Term_text_cocoa;
4249     newterm->pict_hook = Term_pict_cocoa;
4250     /* newterm->mbcs_hook = Term_mbcs_cocoa; */
4251
4252     /* Global pointer */
4253     angband_term[termIndex] = newterm;
4254 }
4255
4256 /**
4257  * Allocate the primary Angband terminal and activate it.  Allocate the other
4258  * Angband terminals.
4259  */
4260 - (void)initWindows {
4261     for (int i = 0; i < ANGBAND_TERM_MAX; i++) {
4262         [self linkTermData:i];
4263     }
4264
4265     Term_activate(angband_term[0]);
4266 }
4267
4268 /**
4269  * Load preferences from preferences file for current host+current user+
4270  * current application.
4271  */
4272 - (void)loadPrefs
4273 {
4274     NSUserDefaults *defs = [NSUserDefaults angbandDefaults];
4275
4276     /* Make some default defaults */
4277     NSMutableArray *defaultTerms = [[NSMutableArray alloc] init];
4278
4279     /*
4280      * The following default rows/cols were determined experimentally by first
4281      * finding the ideal window/font size combinations. But because of awful
4282      * temporal coupling in Term_init_cocoa(), it's impossible to set up the
4283      * defaults there, so we do it this way.
4284      */
4285     for (NSUInteger i = 0; i < ANGBAND_TERM_MAX; i++) {
4286         int columns, rows;
4287         BOOL visible = YES;
4288
4289         switch (i) {
4290         case 0:
4291             columns = 129;
4292             rows = 32;
4293             break;
4294         case 1:
4295             columns = 84;
4296             rows = 20;
4297             break;
4298         case 2:
4299             columns = 42;
4300             rows = 24;
4301             break;
4302         case 3:
4303             columns = 42;
4304             rows = 20;
4305             break;
4306         case 4:
4307             columns = 42;
4308             rows = 16;
4309             break;
4310         case 5:
4311             columns = 84;
4312             rows = 20;
4313             break;
4314         default:
4315             columns = 80;
4316             rows = 24;
4317             visible = NO;
4318             break;
4319         }
4320
4321         NSDictionary *standardTerm =
4322             [NSDictionary dictionaryWithObjectsAndKeys:
4323                           [NSNumber numberWithInt: rows], AngbandTerminalRowsDefaultsKey,
4324                           [NSNumber numberWithInt: columns], AngbandTerminalColumnsDefaultsKey,
4325                           [NSNumber numberWithBool: visible], AngbandTerminalVisibleDefaultsKey,
4326                           nil];
4327         [defaultTerms addObject: standardTerm];
4328     }
4329
4330     NSDictionary *defaults = [[NSDictionary alloc] initWithObjectsAndKeys:
4331 #ifdef JP
4332                               @"Osaka", @"FontName",
4333 #else
4334                               @"Menlo", @"FontName",
4335 #endif
4336                               [NSNumber numberWithFloat:13.f], @"FontSize",
4337                               [NSNumber numberWithInt:60], AngbandFrameRateDefaultsKey,
4338                               [NSNumber numberWithBool:YES], AngbandSoundDefaultsKey,
4339                               [NSNumber numberWithInt:GRAPHICS_NONE], AngbandGraphicsDefaultsKey,
4340                               [NSNumber numberWithBool:YES], AngbandBigTileDefaultsKey,
4341                               defaultTerms, AngbandTerminalsDefaultsKey,
4342                               nil];
4343     [defs registerDefaults:defaults];
4344
4345     /* Preferred graphics mode */
4346     graf_mode_req = [defs integerForKey:AngbandGraphicsDefaultsKey];
4347     if (graf_mode_req != GRAPHICS_NONE &&
4348         get_graphics_mode(graf_mode_req)->grafID != GRAPHICS_NONE &&
4349         [defs boolForKey:AngbandBigTileDefaultsKey] == YES) {
4350         use_bigtile = TRUE;
4351         arg_bigtile = TRUE;
4352     } else {
4353         use_bigtile = FALSE;
4354         arg_bigtile = FALSE;
4355     }
4356
4357     /* Use sounds; set the Angband global */
4358     if ([defs boolForKey:AngbandSoundDefaultsKey] == YES) {
4359         use_sound = TRUE;
4360         [AngbandSoundCatalog sharedSounds].enabled = YES;
4361     } else {
4362         use_sound = FALSE;
4363         [AngbandSoundCatalog sharedSounds].enabled = NO;
4364     }
4365
4366     /* fps */
4367     frames_per_second = [defs integerForKey:AngbandFrameRateDefaultsKey];
4368
4369     /* Font */
4370     [AngbandContext
4371         setDefaultFont:[NSFont fontWithName:[defs valueForKey:@"FontName-0"]
4372                                size:[defs floatForKey:@"FontSize-0"]]];
4373     if (! [AngbandContext defaultFont])
4374         [AngbandContext
4375             setDefaultFont:[NSFont fontWithName:@"Menlo" size:13.]];
4376 }
4377
4378 /**
4379  * Entry point for initializing Angband
4380  */
4381 - (void)beginGame
4382 {
4383     @autoreleasepool {
4384         /* Hooks in some "z-util.c" hooks */
4385         plog_aux = hook_plog;
4386         quit_aux = hook_quit;
4387
4388         /* Initialize file paths */
4389         prepare_paths_and_directories();
4390
4391         /* Note the "system" */
4392         ANGBAND_SYS = "coc";
4393
4394         /* Load possible graphics modes */
4395         init_graphics_modes();
4396
4397         /* Load preferences */
4398         [self loadPrefs];
4399
4400         /* Prepare the windows */
4401         [self initWindows];
4402
4403         /* Set up game event handlers */
4404         /* init_display(); */
4405
4406         /* Register the sound hook */
4407         /* sound_hook = play_sound; */
4408
4409         /* Initialise game */
4410         init_angband();
4411
4412         /* Initialize some save file stuff */
4413         player_egid = getegid();
4414
4415         /* We are now initialized */
4416         initialized = TRUE;
4417
4418         /* Handle "open_when_ready" */
4419         handle_open_when_ready();
4420
4421         /* Handle pending events (most notably update) and flush input */
4422         Term_flush();
4423
4424         /* Prompt the user */
4425         int message_row = (Term->hgt - 23) / 5 + 23;
4426         Term_erase(0, message_row, 255);
4427         put_str(
4428 #ifdef JP
4429             "['ファイル' メニューから '新' または '開く' を選択します]",
4430             message_row, (Term->wid - 57) / 2
4431 #else
4432             "[Choose 'New' or 'Open' from the 'File' menu]",
4433             message_row, (Term->wid - 45) / 2
4434 #endif
4435         );
4436         Term_fresh();
4437     }
4438
4439     while (!game_in_progress) {
4440         @autoreleasepool {
4441             NSEvent *event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantFuture] inMode:NSDefaultRunLoopMode dequeue:YES];
4442             if (event) [NSApp sendEvent:event];
4443         }
4444     }
4445
4446     /*
4447      * Play a game -- "new_game" is set by "new", "open" or the open document
4448      * even handler as appropriate
4449      */
4450     Term_fresh();
4451     play_game(new_game);
4452
4453     quit(NULL);
4454 }
4455
4456 /**
4457  * Implement NSObject's validateMenuItem() method to override enabling or
4458  * disabling a menu item.  Note that, as of 10.14, validateMenuItem() is
4459  * deprecated in NSObject - it will be removed at some point and  the
4460  * application delegate will have to be declared as implementing the
4461  * NSMenuItemValidation protocol.
4462  */
4463 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
4464 {
4465     SEL sel = [menuItem action];
4466     NSInteger tag = [menuItem tag];
4467
4468     if( tag >= AngbandWindowMenuItemTagBase && tag < AngbandWindowMenuItemTagBase + ANGBAND_TERM_MAX )
4469     {
4470         if( tag == AngbandWindowMenuItemTagBase )
4471         {
4472             /* The main window should always be available and visible */
4473             return YES;
4474         }
4475         else
4476         {
4477             /*
4478              * Another window is only usable after Term_init_cocoa() has
4479              * been called for it.  For Angband if window_flag[i] is nonzero
4480              * then that has happened for window i.  For Hengband, that is
4481              * not the case so also test angband_term[i]->data.
4482              */
4483             NSInteger subwindowNumber = tag - AngbandWindowMenuItemTagBase;
4484             return (angband_term[subwindowNumber]->data != 0
4485                     && window_flag[subwindowNumber] > 0);
4486         }
4487
4488         return NO;
4489     }
4490
4491     if (sel == @selector(newGame:))
4492     {
4493         return ! game_in_progress;
4494     }
4495     else if (sel == @selector(editFont:))
4496     {
4497         return YES;
4498     }
4499     else if (sel == @selector(openGame:))
4500     {
4501         return ! game_in_progress;
4502     }
4503     else if (sel == @selector(setRefreshRate:) &&
4504              [[menuItem parentItem] tag] == 150)
4505     {
4506         NSInteger fps = [[NSUserDefaults standardUserDefaults] integerForKey:AngbandFrameRateDefaultsKey];
4507         [menuItem setState: ([menuItem tag] == fps)];
4508         return YES;
4509     }
4510     else if( sel == @selector(setGraphicsMode:) )
4511     {
4512         NSInteger requestedGraphicsMode = [[NSUserDefaults standardUserDefaults] integerForKey:AngbandGraphicsDefaultsKey];
4513         [menuItem setState: (tag == requestedGraphicsMode)];
4514         return YES;
4515     }
4516     else if( sel == @selector(toggleSound:) )
4517     {
4518         BOOL is_on = [[NSUserDefaults standardUserDefaults]
4519                          boolForKey:AngbandSoundDefaultsKey];
4520
4521         [menuItem setState: ((is_on) ? NSOnState : NSOffState)];
4522         return YES;
4523     }
4524     else if( sel == @selector(sendAngbandCommand:) ||
4525              sel == @selector(saveGame:) )
4526     {
4527         /*
4528          * we only want to be able to send commands during an active game
4529          * after the birth screens
4530          */
4531         return !!game_in_progress && character_generated;
4532     }
4533     else return YES;
4534 }
4535
4536
4537 - (IBAction)setRefreshRate:(NSMenuItem *)menuItem
4538 {
4539     frames_per_second = [menuItem tag];
4540     [[NSUserDefaults angbandDefaults] setInteger:frames_per_second forKey:AngbandFrameRateDefaultsKey];
4541 }
4542
4543 - (void)setGraphicsMode:(NSMenuItem *)sender
4544 {
4545     /* We stashed the graphics mode ID in the menu item's tag */
4546     graf_mode_req = [sender tag];
4547
4548     /* Stash it in UserDefaults */
4549     [[NSUserDefaults angbandDefaults] setInteger:graf_mode_req forKey:AngbandGraphicsDefaultsKey];
4550     [[NSUserDefaults angbandDefaults] synchronize];
4551
4552     if (graf_mode_req == GRAPHICS_NONE ||
4553         get_graphics_mode(graf_mode_req) == GRAPHICS_NONE) {
4554         if (use_bigtile) {
4555             arg_bigtile = FALSE;
4556         }
4557     } else if ([[NSUserDefaults angbandDefaults] boolForKey:AngbandBigTileDefaultsKey] == YES &&
4558                ! use_bigtile) {
4559         arg_bigtile = TRUE;
4560     }
4561
4562     if (game_in_progress)
4563     {
4564         if (arg_bigtile != use_bigtile) {
4565             Term_activate(angband_term[0]);
4566             Term_resize(angband_term[0]->wid, angband_term[0]->hgt);
4567         }
4568
4569         /* Hack -- Force redraw */
4570         do_cmd_redraw();
4571
4572         /* Wake up the event loop so it notices the change */
4573         wakeup_event_loop();
4574     }
4575 }
4576
4577 - (void)selectWindow: (id)sender
4578 {
4579     NSInteger subwindowNumber =
4580         [(NSMenuItem *)sender tag] - AngbandWindowMenuItemTagBase;
4581     AngbandContext *context =
4582         (__bridge AngbandContext*) (angband_term[subwindowNumber]->data);
4583     [context.primaryWindow makeKeyAndOrderFront: self];
4584     [context saveWindowVisibleToDefaults: YES];
4585 }
4586
4587 - (IBAction) toggleSound: (NSMenuItem *) sender
4588 {
4589     BOOL is_on = (sender.state == NSOnState);
4590
4591     /* Toggle the state and update the Angband global and preferences. */
4592     if (is_on) {
4593         sender.state = NSOffState;
4594         use_sound = FALSE;
4595         [AngbandSoundCatalog sharedSounds].enabled = NO;
4596     } else {
4597         sender.state = NSOnState;
4598         use_sound = TRUE;
4599         [AngbandSoundCatalog sharedSounds].enabled = YES;
4600     }
4601     [[NSUserDefaults angbandDefaults] setBool:(! is_on)
4602                                       forKey:AngbandSoundDefaultsKey];
4603 }
4604
4605 - (IBAction)toggleWideTiles:(NSMenuItem *) sender
4606 {
4607     BOOL is_on = (sender.state == NSOnState);
4608
4609     /* Toggle the state and update the Angband globals and preferences. */
4610     sender.state = (is_on) ? NSOffState : NSOnState;
4611     [[NSUserDefaults angbandDefaults] setBool:(! is_on)
4612                                       forKey:AngbandBigTileDefaultsKey];
4613     [[NSUserDefaults angbandDefaults] synchronize];
4614     if (graphics_are_enabled()) {
4615         arg_bigtile = (is_on) ? FALSE : TRUE;
4616         /* Mimics the logic in setGraphicsMode(). */
4617         if (game_in_progress && arg_bigtile != use_bigtile) {
4618             Term_activate(angband_term[0]);
4619             Term_resize(angband_term[0]->wid, angband_term[0]->hgt);
4620             do_cmd_redraw();
4621             wakeup_event_loop();
4622         }
4623     }
4624 }
4625
4626 - (void)prepareWindowsMenu
4627 {
4628     @autoreleasepool {
4629         /*
4630          * Get the window menu with default items and add a separator and
4631          * item for the main window.
4632          */
4633         NSMenu *windowsMenu = [[NSApplication sharedApplication] windowsMenu];
4634         [windowsMenu addItem: [NSMenuItem separatorItem]];
4635
4636         NSString *title1 = [NSString stringWithCString:angband_term_name[0]
4637 #ifdef JP
4638                                      encoding:NSJapaneseEUCStringEncoding
4639 #else
4640                                      encoding:NSMacOSRomanStringEncoding
4641 #endif
4642         ];
4643         NSMenuItem *angbandItem = [[NSMenuItem alloc] initWithTitle:title1 action: @selector(selectWindow:) keyEquivalent: @"0"];
4644         [angbandItem setTarget: self];
4645         [angbandItem setTag: AngbandWindowMenuItemTagBase];
4646         [windowsMenu addItem: angbandItem];
4647
4648         /* Add items for the additional term windows */
4649         for( NSInteger i = 1; i < ANGBAND_TERM_MAX; i++ )
4650         {
4651             NSString *title = [NSString stringWithCString:angband_term_name[i]
4652 #ifdef JP
4653                                         encoding:NSJapaneseEUCStringEncoding
4654 #else
4655                                         encoding:NSMacOSRomanStringEncoding
4656 #endif
4657             ];
4658             NSString *keyEquivalent =
4659                 [NSString stringWithFormat: @"%ld", (long)i];
4660             NSMenuItem *windowItem =
4661                 [[NSMenuItem alloc] initWithTitle: title
4662                                     action: @selector(selectWindow:)
4663                                     keyEquivalent: keyEquivalent];
4664             [windowItem setTarget: self];
4665             [windowItem setTag: AngbandWindowMenuItemTagBase + i];
4666             [windowsMenu addItem: windowItem];
4667         }
4668     }
4669 }
4670
4671 /**
4672  *  Send a command to Angband via a menu item. This places the appropriate key
4673  * down events into the queue so that it seems like the user pressed them
4674  * (instead of trying to use the term directly).
4675  */
4676 - (void)sendAngbandCommand: (id)sender
4677 {
4678     NSMenuItem *menuItem = (NSMenuItem *)sender;
4679     NSString *command = [self.commandMenuTagMap objectForKey: [NSNumber numberWithInteger: [menuItem tag]]];
4680     AngbandContext* context =
4681         (__bridge AngbandContext*) (angband_term[0]->data);
4682     NSInteger windowNumber = [context.primaryWindow windowNumber];
4683
4684     /* Send a \ to bypass keymaps */
4685     NSEvent *escape = [NSEvent keyEventWithType: NSKeyDown
4686                                        location: NSZeroPoint
4687                                   modifierFlags: 0
4688                                       timestamp: 0.0
4689                                    windowNumber: windowNumber
4690                                         context: nil
4691                                      characters: @"\\"
4692                     charactersIgnoringModifiers: @"\\"
4693                                       isARepeat: NO
4694                                         keyCode: 0];
4695     [[NSApplication sharedApplication] postEvent: escape atStart: NO];
4696
4697     /* Send the actual command (from the original command set) */
4698     NSEvent *keyDown = [NSEvent keyEventWithType: NSKeyDown
4699                                         location: NSZeroPoint
4700                                    modifierFlags: 0
4701                                        timestamp: 0.0
4702                                     windowNumber: windowNumber
4703                                          context: nil
4704                                       characters: command
4705                      charactersIgnoringModifiers: command
4706                                        isARepeat: NO
4707                                          keyCode: 0];
4708     [[NSApplication sharedApplication] postEvent: keyDown atStart: NO];
4709 }
4710
4711 /**
4712  *  Set up the command menu dynamically, based on CommandMenu.plist.
4713  */
4714 - (void)prepareCommandMenu
4715 {
4716     @autoreleasepool {
4717         NSString *commandMenuPath =
4718             [[NSBundle mainBundle] pathForResource: @"CommandMenu"
4719                                    ofType: @"plist"];
4720         NSArray *commandMenuItems =
4721             [[NSArray alloc] initWithContentsOfFile: commandMenuPath];
4722         NSMutableDictionary *angbandCommands =
4723             [[NSMutableDictionary alloc] init];
4724         NSString *tblname = @"CommandMenu";
4725         NSInteger tagOffset = 0;
4726
4727         for( NSDictionary *item in commandMenuItems )
4728         {
4729             BOOL useShiftModifier =
4730                 [[item valueForKey: @"ShiftModifier"] boolValue];
4731             BOOL useOptionModifier =
4732                 [[item valueForKey: @"OptionModifier"] boolValue];
4733             NSUInteger keyModifiers = NSCommandKeyMask;
4734             keyModifiers |= (useShiftModifier) ? NSShiftKeyMask : 0;
4735             keyModifiers |= (useOptionModifier) ? NSAlternateKeyMask : 0;
4736
4737             NSString *lookup = [item valueForKey: @"Title"];
4738             NSString *title = NSLocalizedStringWithDefaultValue(
4739                 lookup, tblname, [NSBundle mainBundle], lookup, @"");
4740             NSString *key = [item valueForKey: @"KeyEquivalent"];
4741             NSMenuItem *menuItem =
4742                 [[NSMenuItem alloc] initWithTitle: title
4743                                     action: @selector(sendAngbandCommand:)
4744                                     keyEquivalent: key];
4745             [menuItem setTarget: self];
4746             [menuItem setKeyEquivalentModifierMask: keyModifiers];
4747             [menuItem setTag: AngbandCommandMenuItemTagBase + tagOffset];
4748             [self.commandMenu addItem: menuItem];
4749
4750             NSString *angbandCommand = [item valueForKey: @"AngbandCommand"];
4751             [angbandCommands setObject: angbandCommand
4752                              forKey: [NSNumber numberWithInteger: [menuItem tag]]];
4753             tagOffset++;
4754         }
4755
4756         self.commandMenuTagMap = [[NSDictionary alloc]
4757                                      initWithDictionary: angbandCommands];
4758     }
4759 }
4760
4761 - (void)awakeFromNib
4762 {
4763     [super awakeFromNib];
4764
4765     [self prepareWindowsMenu];
4766     [self prepareCommandMenu];
4767 }
4768
4769 - (void)applicationDidFinishLaunching:sender
4770 {
4771     [self beginGame];
4772     
4773     /* Once beginGame finished, the game is over - that's how Angband works,
4774          * and we should quit */
4775     game_is_finished = TRUE;
4776     [NSApp terminate:self];
4777 }
4778
4779 - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
4780 {
4781     if (p_ptr->playing == FALSE || game_is_finished == TRUE)
4782     {
4783         return NSTerminateNow;
4784     }
4785     else if (! inkey_flag)
4786     {
4787         /* For compatibility with other ports, do not quit in this case */
4788         return NSTerminateCancel;
4789     }
4790     else
4791     {
4792         /* Stop playing */
4793         /* player->upkeep->playing = FALSE; */
4794
4795         /* Post an escape event so that we can return from our get-key-event
4796                  * function */
4797         wakeup_event_loop();
4798         quit_when_ready = true;
4799         /* Must return Cancel, not Later, because we need to get out of the
4800                  * run loop and back to Angband's loop */
4801         return NSTerminateCancel;
4802     }
4803 }
4804
4805 /**
4806  * Dynamically build the Graphics menu
4807  */
4808 - (void)menuNeedsUpdate:(NSMenu *)menu {
4809     
4810     /* Only the graphics menu is dynamic */
4811     if (! [menu isEqual:self.graphicsMenu])
4812         return;
4813     
4814     /* If it's non-empty, then we've already built it. Currently graphics modes
4815          * won't change once created; if they ever can we can remove this check.
4816      * Note that the check mark does change, but that's handled in
4817          * validateMenuItem: instead of menuNeedsUpdate: */
4818     if ([menu numberOfItems] > 0)
4819         return;
4820     
4821     /* This is the action for all these menu items */
4822     SEL action = @selector(setGraphicsMode:);
4823     
4824     /* Add an initial Classic ASCII menu item */
4825     NSString *tblname = @"GraphicsMenu";
4826     NSString *key = @"Classic ASCII";
4827     NSString *title = NSLocalizedStringWithDefaultValue(
4828         key, tblname, [NSBundle mainBundle], key, @"");
4829     NSMenuItem *classicItem = [menu addItemWithTitle:title action:action keyEquivalent:@""];
4830     [classicItem setTag:GRAPHICS_NONE];
4831     
4832     /* Walk through the list of graphics modes */
4833     if (graphics_modes) {
4834         NSInteger i;
4835
4836         for (i=0; graphics_modes[i].pNext; i++)
4837         {
4838             const graphics_mode *graf = &graphics_modes[i];
4839
4840             if (graf->grafID == GRAPHICS_NONE) {
4841                 continue;
4842             }
4843             /* Make the title. NSMenuItem throws on a nil title, so ensure it's
4844                    * not nil. */
4845             key = [[NSString alloc] initWithUTF8String:graf->menuname];
4846             title = NSLocalizedStringWithDefaultValue(
4847                 key, tblname, [NSBundle mainBundle], key, @"");
4848
4849             /* Make the item */
4850             NSMenuItem *item = [menu addItemWithTitle:title action:action keyEquivalent:@""];
4851             [item setTag:graf->grafID];
4852         }
4853     }
4854 }
4855
4856 /**
4857  * Delegate method that gets called if we're asked to open a file.
4858  */
4859 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
4860 {
4861     /* Can't open a file once we've started */
4862     if (game_in_progress) {
4863         [[NSApplication sharedApplication]
4864             replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
4865         return;
4866     }
4867
4868     /* We can only open one file. Use the last one. */
4869     NSString *file = [filenames lastObject];
4870     if (! file) {
4871         [[NSApplication sharedApplication]
4872             replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
4873         return;
4874     }
4875
4876     /* Put it in savefile */
4877     if (! [file getFileSystemRepresentation:savefile maxLength:sizeof savefile]) {
4878         [[NSApplication sharedApplication]
4879             replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
4880         return;
4881     }
4882
4883     game_in_progress = TRUE;
4884     new_game = FALSE;
4885
4886     /* Wake us up in case this arrives while we're sitting at the Welcome
4887          * screen! */
4888     wakeup_event_loop();
4889
4890     [[NSApplication sharedApplication]
4891         replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
4892 }
4893
4894 @end
4895
4896 int main(int argc, char* argv[])
4897 {
4898     NSApplicationMain(argc, (void*)argv);
4899     return (0);
4900 }
4901
4902 #endif /* MACINTOSH || MACH_O_COCOA */