OSDN Git Service

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