OSDN Git Service

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