OSDN Git Service

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