(svn r28004) -Update from Eints:
[openttd.git] / src / video / cocoa / cocoa_v.mm
blob4df7cb0035dc9c9d8969da62292a6e61a9fe588b
1 /* $Id$ */
3 /*
4  * This file is part of OpenTTD.
5  * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
6  * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
7  * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
8  */
10 /** @file cocoa_v.mm Code related to the cocoa video driver(s). */
12 /******************************************************************************
13  *                             Cocoa video driver                             *
14  * Known things left to do:                                                   *
15  *  Nothing at the moment.                                                    *
16  ******************************************************************************/
18 #ifdef WITH_COCOA
20 #include "../../stdafx.h"
21 #include "../../os/macosx/macos.h"
23 #define Rect  OTTDRect
24 #define Point OTTDPoint
25 #import <Cocoa/Cocoa.h>
26 #undef Rect
27 #undef Point
29 #include "../../openttd.h"
30 #include "../../debug.h"
31 #include "../../core/geometry_type.hpp"
32 #include "cocoa_v.h"
33 #include "../../blitter/factory.hpp"
34 #include "../../fileio_func.h"
35 #include "../../gfx_func.h"
36 #include "../../window_func.h"
37 #include "../../window_gui.h"
39 #import <sys/param.h> /* for MAXPATHLEN */
41 /**
42  * Important notice regarding all modifications!!!!!!!
43  * There are certain limitations because the file is objective C++.
44  * gdb has limitations.
45  * C++ and objective C code can't be joined in all cases (classes stuff).
46  * Read http://developer.apple.com/releasenotes/Cocoa/Objective-C++.html for more information.
47  */
50 @interface OTTDMain : NSObject
51 @end
54 static NSAutoreleasePool *_ottd_autorelease_pool;
55 static OTTDMain *_ottd_main;
56 static bool _cocoa_video_started = false;
57 static bool _cocoa_video_dialog = false;
59 CocoaSubdriver *_cocoa_subdriver = NULL;
61 static NSString *OTTDMainLaunchGameEngine = @"ottdmain_launch_game_engine";
64 /**
65  * The main class of the application, the application's delegate.
66  */
67 @implementation OTTDMain
68 /**
69  * Stop the game engine. Must be called on main thread.
70  */
71 - (void)stopEngine
73         [ NSApp stop:self ];
75         /* Send an empty event to return from the run loop. Without that, application is stuck waiting for an event. */
76         NSEvent *event = [ NSEvent otherEventWithType:NSApplicationDefined location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0.0 windowNumber:0 context:nil subtype:0 data1:0 data2:0 ];
77         [ NSApp postEvent:event atStart:YES ];
80 /**
81  * Start the game loop.
82  */
83 - (void)launchGameEngine: (NSNotification*) note
85         /* Setup cursor for the current _game_mode. */
86         [ _cocoa_subdriver->cocoaview resetCursorRects ];
88         /* Hand off to main application code. */
89         QZ_GameLoop();
91         /* We are done, thank you for playing. */
92         [ self performSelectorOnMainThread:@selector(stopEngine) withObject:nil waitUntilDone:FALSE ];
95 /**
96  * Called when the internal event loop has just started running.
97  */
98 - (void) applicationDidFinishLaunching: (NSNotification*) note
100         /* Add a notification observer so we can restart the game loop later on if necessary. */
101         [ [ NSNotificationCenter defaultCenter ] addObserver:self selector:@selector(launchGameEngine:) name:OTTDMainLaunchGameEngine object:nil ];
103         /* Start game loop. */
104         [ [ NSNotificationCenter defaultCenter ] postNotificationName:OTTDMainLaunchGameEngine object:nil ];
108  * Display the in game quit confirmation dialog.
109  */
110 - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*) sender
112         HandleExitGameRequest();
114         return NSTerminateCancel; // NSTerminateLater ?
118  * Remove ourself as a notification observer.
119  */
120 - (void)unregisterObserver
122         [ [ NSNotificationCenter defaultCenter ] removeObserver:self ];
124 @end
127  * Initialize the application menu shown in top bar.
128  */
129 static void setApplicationMenu()
131         NSString *appName = @"OpenTTD";
132         NSMenu *appleMenu = [ [ NSMenu alloc ] initWithTitle:appName ];
134         /* Add menu items */
135         NSString *title = [ @"About " stringByAppendingString:appName ];
136         [ appleMenu addItemWithTitle:title action:@selector(orderFrontStandardAboutPanel:) keyEquivalent:@"" ];
138         [ appleMenu addItem:[ NSMenuItem separatorItem ] ];
140         title = [ @"Hide " stringByAppendingString:appName ];
141         [ appleMenu addItemWithTitle:title action:@selector(hide:) keyEquivalent:@"h" ];
143         NSMenuItem *menuItem = [ appleMenu addItemWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h" ];
144         [ menuItem setKeyEquivalentModifierMask:(NSAlternateKeyMask | NSCommandKeyMask) ];
146         [ appleMenu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@"" ];
148         [ appleMenu addItem:[ NSMenuItem separatorItem ] ];
150         title = [ @"Quit " stringByAppendingString:appName ];
151         [ appleMenu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q" ];
153         /* Put menu into the menubar */
154         menuItem = [ [ NSMenuItem alloc ] initWithTitle:@"" action:nil keyEquivalent:@"" ];
155         [ menuItem setSubmenu:appleMenu ];
156         [ [ NSApp mainMenu ] addItem:menuItem ];
158         /* Tell the application object that this is now the application menu.
159          * This interesting Objective-C construct is used because not all SDK
160          * versions define this method publicly. */
161         [ NSApp performSelector:@selector(setAppleMenu:) withObject:appleMenu ];
163         /* Finally give up our references to the objects */
164         [ appleMenu release ];
165         [ menuItem release ];
169  * Create a window menu.
170  */
171 static void setupWindowMenu()
173         NSMenu *windowMenu = [ [ NSMenu alloc ] initWithTitle:@"Window" ];
175         /* "Minimize" item */
176         [ windowMenu addItemWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m" ];
178         /* Put menu into the menubar */
179         NSMenuItem *menuItem = [ [ NSMenuItem alloc ] initWithTitle:@"Window" action:nil keyEquivalent:@"" ];
180         [ menuItem setSubmenu:windowMenu ];
181         [ [ NSApp mainMenu ] addItem:menuItem ];
183         if (MacOSVersionIsAtLeast(10, 7, 0)) {
184                 /* The OS will change the name of this menu item automatically */
185                 [ windowMenu addItemWithTitle:@"Fullscreen" action:@selector(toggleFullScreen:) keyEquivalent:@"^f" ];
186         }
188         /* Tell the application object that this is now the window menu */
189         [ NSApp setWindowsMenu:windowMenu ];
191         /* Finally give up our references to the objects */
192         [ windowMenu release ];
193         [ menuItem release ];
197  * Startup the application.
198  */
199 static void setupApplication()
201         ProcessSerialNumber psn = { 0, kCurrentProcess };
203         /* Ensure the application object is initialised */
204         [ NSApplication sharedApplication ];
206 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_3
207         /* Tell the dock about us */
208         if (MacOSVersionIsAtLeast(10, 3, 0)) {
209                 OSStatus returnCode = TransformProcessType(&psn, kProcessTransformToForegroundApplication);
210                 if (returnCode != 0) DEBUG(driver, 0, "Could not change to foreground application. Error %d", (int)returnCode);
211         }
212 #endif
214         /* Become the front process, important when start from the command line. */
215         OSErr err = SetFrontProcess(&psn);
216         if (err != 0) DEBUG(driver, 0, "Could not bring the application to front. Error %d", (int)err);
218         /* Set up the menubar */
219         [ NSApp setMainMenu:[ [ NSMenu alloc ] init ] ];
220         setApplicationMenu();
221         setupWindowMenu();
223         /* Create OTTDMain and make it the app delegate */
224         _ottd_main = [ [ OTTDMain alloc ] init ];
225         [ NSApp setDelegate:_ottd_main ];
229 static int CDECL ModeSorter(const OTTD_Point *p1, const OTTD_Point *p2)
231         if (p1->x < p2->x) return -1;
232         if (p1->x > p2->x) return +1;
233         if (p1->y < p2->y) return -1;
234         if (p1->y > p2->y) return +1;
235         return 0;
238 uint QZ_ListModes(OTTD_Point *modes, uint max_modes, CGDirectDisplayID display_id, int device_depth)
240         CFArrayRef mode_list  = CGDisplayAvailableModes(display_id);
241         CFIndex    num_modes = CFArrayGetCount(mode_list);
243         /* Build list of modes with the requested bpp */
244         uint count = 0;
245         for (CFIndex i = 0; i < num_modes && count < max_modes; i++) {
246                 int intvalue, bpp;
247                 uint16 width, height;
249                 CFDictionaryRef onemode = (const __CFDictionary*)CFArrayGetValueAtIndex(mode_list, i);
250                 CFNumberRef number = (const __CFNumber*)CFDictionaryGetValue(onemode, kCGDisplayBitsPerPixel);
251                 CFNumberGetValue(number, kCFNumberSInt32Type, &bpp);
253                 if (bpp != device_depth) continue;
255                 number = (const __CFNumber*)CFDictionaryGetValue(onemode, kCGDisplayWidth);
256                 CFNumberGetValue(number, kCFNumberSInt32Type, &intvalue);
257                 width = (uint16)intvalue;
259                 number = (const __CFNumber*)CFDictionaryGetValue(onemode, kCGDisplayHeight);
260                 CFNumberGetValue(number, kCFNumberSInt32Type, &intvalue);
261                 height = (uint16)intvalue;
263                 /* Check if mode is already in the list */
264                 bool hasMode = false;
265                 for (uint i = 0; i < count; i++) {
266                         if (modes[i].x == width &&  modes[i].y == height) {
267                                 hasMode = true;
268                                 break;
269                         }
270                 }
272                 if (hasMode) continue;
274                 /* Add mode to the list */
275                 modes[count].x = width;
276                 modes[count].y = height;
277                 count++;
278         }
280         /* Sort list smallest to largest */
281         QSortT(modes, count, &ModeSorter);
283         return count;
286 /** Small function to test if the main display can display 8 bpp in fullscreen */
287 bool QZ_CanDisplay8bpp()
289         /* 8bpp modes are deprecated starting in 10.5. CoreGraphics will return them
290          * as available in the display list, but many features (e.g. palette animation)
291          * will be broken. */
292         if (MacOSVersionIsAtLeast(10, 5, 0)) return false;
294         OTTD_Point p;
296         /* We want to know if 8 bpp is possible in fullscreen and not anything about
297          * resolutions. Because of this we want to fill a list of 1 resolution of 8 bpp
298          * on display 0 (main) and return if we found one. */
299         return QZ_ListModes(&p, 1, 0, 8);
303  * Update the video modus.
305  * @pre _cocoa_subdriver != NULL
306  */
307 static void QZ_UpdateVideoModes()
309         assert(_cocoa_subdriver != NULL);
311         OTTD_Point modes[32];
312         uint count = _cocoa_subdriver->ListModes(modes, lengthof(modes));
314         for (uint i = 0; i < count; i++) {
315                 _resolutions[i].width  = modes[i].x;
316                 _resolutions[i].height = modes[i].y;
317         }
319         _num_resolutions = count;
323  * Handle a change of the display area.
324  */
325 void QZ_GameSizeChanged()
327         if (_cocoa_subdriver == NULL) return;
329         /* Tell the game that the resolution has changed */
330         _screen.width = _cocoa_subdriver->GetWidth();
331         _screen.height = _cocoa_subdriver->GetHeight();
332         _screen.pitch = _cocoa_subdriver->GetWidth();
333         _screen.dst_ptr = _cocoa_subdriver->GetPixelBuffer();
334         _fullscreen = _cocoa_subdriver->IsFullscreen();
336         BlitterFactory::GetCurrentBlitter()->PostResize();
338         GameSizeChanged();
342  * Find a suitable cocoa window subdriver.
344  * @param width Width of display area.
345  * @param height Height of display area.
346  * @param bpp Colour depth of display area.
347  * @return Pointer to window subdriver.
348  */
349 static CocoaSubdriver *QZ_CreateWindowSubdriver(int width, int height, int bpp)
351 #if defined(ENABLE_COCOA_QUARTZ) || defined(ENABLE_COCOA_QUICKDRAW)
352         CocoaSubdriver *ret;
353 #endif
355 #if defined(ENABLE_COCOA_QUARTZ) && (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4)
356         /* The reason for the version mismatch is due to the fact that the 10.4 binary needs to work on 10.5 as well. */
357         if (MacOSVersionIsAtLeast(10, 5, 0)) {
358                 ret = QZ_CreateWindowQuartzSubdriver(width, height, bpp);
359                 if (ret != NULL) return ret;
360         }
361 #endif
363 #ifdef ENABLE_COCOA_QUICKDRAW
364         ret = QZ_CreateWindowQuickdrawSubdriver(width, height, bpp);
365         if (ret != NULL) return ret;
366 #endif
368 #if defined(ENABLE_COCOA_QUARTZ) && (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4)
369         /*
370          * If we get here we are running 10.4 or earlier and either openttd was compiled without the QuickDraw driver
371          * or it failed to load for some reason. Fall back to Quartz if possible even though that driver is slower.
372          */
373         if (MacOSVersionIsAtLeast(10, 4, 0)) {
374                 ret = QZ_CreateWindowQuartzSubdriver(width, height, bpp);
375                 if (ret != NULL) return ret;
376         }
377 #endif
379         return NULL;
383  * Find a suitable cocoa subdriver.
385  * @param width Width of display area.
386  * @param height Height of display area.
387  * @param bpp Colour depth of display area.
388  * @param fullscreen Whether a fullscreen mode is requested.
389  * @param fallback Whether we look for a fallback driver.
390  * @return Pointer to window subdriver.
391  */
392 static CocoaSubdriver *QZ_CreateSubdriver(int width, int height, int bpp, bool fullscreen, bool fallback)
394         CocoaSubdriver *ret = NULL;
395         /* OSX 10.7 allows to toggle fullscreen mode differently */
396         if (MacOSVersionIsAtLeast(10, 7, 0)) {
397                 ret = QZ_CreateWindowSubdriver(width, height, bpp);
398                 if (ret != NULL && fullscreen) ret->ToggleFullscreen();
399         }
400 #if (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_9)
401         else {
402                 ret = fullscreen ? QZ_CreateFullscreenSubdriver(width, height, bpp) : QZ_CreateWindowSubdriver(width, height, bpp);
403         }
404 #endif /* (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_9) */
406         if (ret != NULL) return ret;
407         if (!fallback) return NULL;
409         /* Try again in 640x480 windowed */
410         DEBUG(driver, 0, "Setting video mode failed, falling back to 640x480 windowed mode.");
411         ret = QZ_CreateWindowSubdriver(640, 480, bpp);
412         if (ret != NULL) return ret;
414 #if defined(_DEBUG) && (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_9)
415         /* This Fullscreen mode crashes on OSX 10.7 */
416         if (!MacOSVersionIsAtLeast(10, 7, 0)) {
417                 /* Try fullscreen too when in debug mode */
418                 DEBUG(driver, 0, "Setting video mode failed, falling back to 640x480 fullscreen mode.");
419                 ret = QZ_CreateFullscreenSubdriver(640, 480, bpp);
420                 if (ret != NULL) return ret;
421         }
422 #endif /* defined(_DEBUG) && (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_9) */
424         return NULL;
428 static FVideoDriver_Cocoa iFVideoDriver_Cocoa;
431  * Stop the cocoa video subdriver.
432  */
433 void VideoDriver_Cocoa::Stop()
435         if (!_cocoa_video_started) return;
437         [ _ottd_main unregisterObserver ];
439         delete _cocoa_subdriver;
440         _cocoa_subdriver = NULL;
442         [ _ottd_main release ];
444         _cocoa_video_started = false;
448  * Initialize a cocoa video subdriver.
449  */
450 const char *VideoDriver_Cocoa::Start(const char * const *parm)
452         if (!MacOSVersionIsAtLeast(10, 3, 0)) return "The Cocoa video driver requires Mac OS X 10.3 or later.";
454         if (_cocoa_video_started) return "Already started";
455         _cocoa_video_started = true;
457         setupApplication();
459         /* Don't create a window or enter fullscreen if we're just going to show a dialog. */
460         if (_cocoa_video_dialog) return NULL;
462         int width  = _cur_resolution.width;
463         int height = _cur_resolution.height;
464         int bpp = BlitterFactory::GetCurrentBlitter()->GetScreenDepth();
466         _cocoa_subdriver = QZ_CreateSubdriver(width, height, bpp, _fullscreen, true);
467         if (_cocoa_subdriver == NULL) {
468                 Stop();
469                 return "Could not create subdriver";
470         }
472         QZ_GameSizeChanged();
473         QZ_UpdateVideoModes();
475         return NULL;
479  * Set dirty a rectangle managed by a cocoa video subdriver.
481  * @param left Left x cooordinate of the dirty rectangle.
482  * @param top Uppder y coordinate of the dirty rectangle.
483  * @param width Width of the dirty rectangle.
484  * @param height Height of the dirty rectangle.
485  */
486 void VideoDriver_Cocoa::MakeDirty(int left, int top, int width, int height)
488         assert(_cocoa_subdriver != NULL);
490         _cocoa_subdriver->MakeDirty(left, top, width, height);
494  * Start the main programme loop when using a cocoa video driver.
495  */
496 void VideoDriver_Cocoa::MainLoop()
498         /* Restart game loop if it was already running (e.g. after bootstrapping),
499          * otherwise this call is a no-op. */
500         [ [ NSNotificationCenter defaultCenter ] postNotificationName:OTTDMainLaunchGameEngine object:nil ];
502         /* Start the main event loop. */
503         [ NSApp run ];
507  * Change the resolution when using a cocoa video driver.
509  * @param w New window width.
510  * @param h New window height.
511  * @return Whether the video driver was successfully updated.
512  */
513 bool VideoDriver_Cocoa::ChangeResolution(int w, int h)
515         assert(_cocoa_subdriver != NULL);
517         bool ret = _cocoa_subdriver->ChangeResolution(w, h, BlitterFactory::GetCurrentBlitter()->GetScreenDepth());
519         QZ_GameSizeChanged();
520         QZ_UpdateVideoModes();
522         return ret;
526  * Toggle between windowed and full screen mode for cocoa display driver.
528  * @param full_screen Whether to switch to full screen or not.
529  * @return Whether the mode switch was successful.
530  */
531 bool VideoDriver_Cocoa::ToggleFullscreen(bool full_screen)
533         assert(_cocoa_subdriver != NULL);
535         /* For 10.7 and later, we try to toggle using the quartz subdriver. */
536         if (_cocoa_subdriver->ToggleFullscreen()) return true;
538         bool oldfs = _cocoa_subdriver->IsFullscreen();
539         if (full_screen != oldfs) {
540                 int width  = _cocoa_subdriver->GetWidth();
541                 int height = _cocoa_subdriver->GetHeight();
542                 int bpp    = BlitterFactory::GetCurrentBlitter()->GetScreenDepth();
544                 delete _cocoa_subdriver;
545                 _cocoa_subdriver = NULL;
547                 _cocoa_subdriver = QZ_CreateSubdriver(width, height, bpp, full_screen, false);
548                 if (_cocoa_subdriver == NULL) {
549                         _cocoa_subdriver = QZ_CreateSubdriver(width, height, bpp, oldfs, true);
550                         if (_cocoa_subdriver == NULL) error("Cocoa: Failed to create subdriver");
551                 }
552         }
554         QZ_GameSizeChanged();
555         QZ_UpdateVideoModes();
557         return _cocoa_subdriver->IsFullscreen() == full_screen;
561  * Callback invoked after the blitter was changed.
563  * @return True if no error.
564  */
565 bool VideoDriver_Cocoa::AfterBlitterChange()
567         return this->ChangeResolution(_screen.width, _screen.height);
571  * An edit box lost the input focus. Abort character compositing if necessary.
572  */
573 void VideoDriver_Cocoa::EditBoxLostFocus()
575         if (_cocoa_subdriver != NULL) {
576                 if ([ _cocoa_subdriver->cocoaview respondsToSelector:@selector(inputContext) ] && [ [ _cocoa_subdriver->cocoaview performSelector:@selector(inputContext) ] respondsToSelector:@selector(discardMarkedText) ]) {
577                         [ [ _cocoa_subdriver->cocoaview performSelector:@selector(inputContext) ] performSelector:@selector(discardMarkedText) ];
578                 } else {
579                         [ [ NSInputManager currentInputManager ] markedTextAbandoned:_cocoa_subdriver->cocoaview ];
580                 }
581         }
582         /* Clear any marked string from the current edit box. */
583         HandleTextInput(NULL, true);
587  * Catch asserts prior to initialization of the videodriver.
589  * @param title Window title.
590  * @param message Message text.
591  * @param buttonLabel Button text.
593  * @note This is needed since sometimes assert is called before the videodriver is initialized .
594  */
595 void CocoaDialog(const char *title, const char *message, const char *buttonLabel)
597         _cocoa_video_dialog = true;
599         bool wasstarted = _cocoa_video_started;
600         if (VideoDriver::GetInstance() == NULL) {
601                 setupApplication(); // Setup application before showing dialog
602         } else if (!_cocoa_video_started && VideoDriver::GetInstance()->Start(NULL) != NULL) {
603                 fprintf(stderr, "%s: %s\n", title, message);
604                 return;
605         }
607         NSRunAlertPanel([ NSString stringWithUTF8String:title ], [ NSString stringWithUTF8String:message ], [ NSString stringWithUTF8String:buttonLabel ], nil, nil);
609         if (!wasstarted && VideoDriver::GetInstance() != NULL) VideoDriver::GetInstance()->Stop();
611         _cocoa_video_dialog = false;
614 /** Set the application's bundle directory.
616  * This is needed since OS X application bundles do not have a
617  * current directory and the data files are 'somewhere' in the bundle.
618  */
619 void cocoaSetApplicationBundleDir()
621         char tmp[MAXPATHLEN];
622         CFURLRef url = CFBundleCopyResourcesDirectoryURL(CFBundleGetMainBundle());
623         if (CFURLGetFileSystemRepresentation(url, true, (unsigned char*)tmp, MAXPATHLEN)) {
624                 AppendPathSeparator(tmp, lastof(tmp));
625                 _searchpaths[SP_APPLICATION_BUNDLE_DIR] = stredup(tmp);
626         } else {
627                 _searchpaths[SP_APPLICATION_BUNDLE_DIR] = NULL;
628         }
630         CFRelease(url);
634  * Setup autorelease for the application pool.
636  * These are called from main() to prevent a _NSAutoreleaseNoPool error when
637  * exiting before the cocoa video driver has been loaded
638  */
639 void cocoaSetupAutoreleasePool()
641         _ottd_autorelease_pool = [ [ NSAutoreleasePool alloc ] init ];
645  * Autorelease the application pool.
646  */
647 void cocoaReleaseAutoreleasePool()
649         [ _ottd_autorelease_pool release ];
654  * Re-implement the system cursor in order to allow hiding and showing it nicely
655  */
656 @implementation NSCursor (OTTD_CocoaCursor)
657 + (NSCursor *) clearCocoaCursor
659         /* RAW 16x16 transparent GIF */
660         unsigned char clearGIFBytes[] = {
661                 0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 0x10, 0x00, 0x10, 0x00, 0x80, 0x00,
662                 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0xF9, 0x04, 0x01, 0x00,
663                 0x00, 0x01, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x10, 0x00,
664                 0x00, 0x02, 0x0E, 0x8C, 0x8F, 0xA9, 0xCB, 0xED, 0x0F, 0xA3, 0x9C, 0xB4,
665                 0xDA, 0x8B, 0xB3, 0x3E, 0x05, 0x00, 0x3B};
666         NSData *clearGIFData = [ NSData dataWithBytesNoCopy:&clearGIFBytes[0] length:55 freeWhenDone:NO ];
667         NSImage *clearImg = [ [ NSImage alloc ] initWithData:clearGIFData ];
668         return [ [ NSCursor alloc ] initWithImage:clearImg hotSpot:NSMakePoint(0.0,0.0) ];
670 @end
674 @implementation OTTD_CocoaWindow
676 - (void)setDriver:(CocoaSubdriver*)drv
678         driver = drv;
681  * Minimize the window
682  */
683 - (void)miniaturize:(id)sender
685         /* make the alpha channel opaque so anim won't have holes in it */
686         driver->SetPortAlphaOpaque();
688         /* window is hidden now */
689         driver->active = false;
691         [ super miniaturize:sender ];
695  * This method fires just before the window deminaturizes from the Dock.
696  * We'll save the current visible surface, let the window manager redraw any
697  * UI elements, and restore the surface. This way, no expose event
698  * is required, and the deminiaturize works perfectly.
699  */
700 - (void)display
702         driver->SetPortAlphaOpaque();
704         /* save current visible surface */
705         [ self cacheImageInRect:[ driver->cocoaview frame ] ];
707         /* let the window manager redraw controls, border, etc */
708         [ super display ];
710         /* restore visible surface */
711         [ self restoreCachedImage ];
713         /* window is visible again */
714         driver->active = true;
717  * Define the rectangle we draw our window in
718  */
719 - (void)setFrame:(NSRect)frameRect display:(BOOL)flag
721         [ super setFrame:frameRect display:flag ];
723         /* Don't do anything if the window is currently being created */
724         if (driver->setup) return;
726         if (!driver->WindowResized()) error("Cocoa: Failed to resize window.");
729  * Handle hiding of the application
730  */
731 - (void)appDidHide:(NSNotification*)note
733         driver->active = false;
736  * Fade-in the application and restore display plane
737  */
738 - (void)appWillUnhide:(NSNotification*)note
740         driver->SetPortAlphaOpaque ();
743  * Unhide and restore display plane and re-activate driver
744  */
745 - (void)appDidUnhide:(NSNotification*)note
747         driver->active = true;
750  * Initialize event system for the application rectangle
751  */
752 - (id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)styleMask backing:(NSBackingStoreType)backingType defer:(BOOL)flag
754         /* Make our window subclass receive these application notifications */
755         [ [ NSNotificationCenter defaultCenter ] addObserver:self
756                 selector:@selector(appDidHide:) name:NSApplicationDidHideNotification object:NSApp ];
758         [ [ NSNotificationCenter defaultCenter ] addObserver:self
759                 selector:@selector(appDidUnhide:) name:NSApplicationDidUnhideNotification object:NSApp ];
761         [ [ NSNotificationCenter defaultCenter ] addObserver:self
762                 selector:@selector(appWillUnhide:) name:NSApplicationWillUnhideNotification object:NSApp ];
764         return [ super initWithContentRect:contentRect styleMask:styleMask backing:backingType defer:flag ];
767 @end
772  * Count the number of UTF-16 code points in a range of an UTF-8 string.
773  * @param from Start of the range.
774  * @param to End of the range.
775  * @return Number of UTF-16 code points in the range.
776  */
777 static NSUInteger CountUtf16Units(const char *from, const char *to)
779         NSUInteger i = 0;
781         while (from < to) {
782                 WChar c;
783                 size_t len = Utf8Decode(&c, from);
784                 i += len < 4 ? 1 : 2; // Watch for surrogate pairs.
785                 from += len;
786         }
788         return i;
792  * Advance an UTF-8 string by a number of equivalent UTF-16 code points.
793  * @param str UTF-8 string.
794  * @param count Number of UTF-16 code points to advance the string by.
795  * @return Advanced string pointer.
796  */
797 static const char *Utf8AdvanceByUtf16Units(const char *str, NSUInteger count)
799         for (NSUInteger i = 0; i < count && *str != '\0'; ) {
800                 WChar c;
801                 size_t len = Utf8Decode(&c, str);
802                 i += len < 4 ? 1 : 2; // Watch for surrogates.
803                 str += len;
804         }
806         return str;
809 @implementation OTTD_CocoaView
811  * Initialize the driver
812  */
813 - (void)setDriver:(CocoaSubdriver*)drv
815         driver = drv;
818  * Define the opaqueness of the window / screen
819  * @return opaqueness of window / screen
820  */
821 - (BOOL)isOpaque
823         return YES;
826  * Draws a rectangle on the screen.
827  * It's overwritten by the individual drivers but must be defined
828  */
829 - (void)drawRect:(NSRect)invalidRect
831         return;
834  * Allow to handle events
835  */
836 - (BOOL)acceptsFirstResponder
838         return YES;
841  * Actually handle events
842  */
843 - (BOOL)becomeFirstResponder
845         return YES;
848  * Define the rectangle where we draw our application window
849  */
850 - (void)setTrackingRect
852         NSPoint loc = [ self convertPoint:[ [ self window ] mouseLocationOutsideOfEventStream ] fromView:nil ];
853         BOOL inside = ([ self hitTest:loc ]==self);
854         if (inside) [ [ self window ] makeFirstResponder:self ];
855         trackingtag = [ self addTrackingRect:[ self visibleRect ] owner:self userData:nil assumeInside:inside ];
858  * Return responsibility for the application window to system
859  */
860 - (void)clearTrackingRect
862         [ self removeTrackingRect:trackingtag ];
865  * Declare responsibility for the cursor within our application rect
866  */
867 - (void)resetCursorRects
869         [ super resetCursorRects ];
870         [ self clearTrackingRect ];
871         [ self setTrackingRect ];
872         [ self addCursorRect:[ self bounds ] cursor:(_game_mode == GM_BOOTSTRAP ? [ NSCursor arrowCursor ] : [ NSCursor clearCocoaCursor ]) ];
875  * Prepare for moving the application window
876  */
877 - (void)viewWillMoveToWindow:(NSWindow *)win
879         if (!win && [ self window ]) [ self clearTrackingRect ];
882  * Restore our responsibility for our application window after moving
883  */
884 - (void)viewDidMoveToWindow
886         if ([ self window ]) [ self setTrackingRect ];
889  * Make OpenTTD aware that it has control over the mouse
890  */
891 - (void)mouseEntered:(NSEvent *)theEvent
893         _cursor.in_window = true;
896  * Make OpenTTD aware that it has NOT control over the mouse
897  */
898 - (void)mouseExited:(NSEvent *)theEvent
900         if (_cocoa_subdriver != NULL) UndrawMouseCursor();
901         _cursor.in_window = false;
905 /** Insert the given text at the given range. */
906 - (void)insertText:(id)aString replacementRange:(NSRange)replacementRange
908         if (!EditBoxInGlobalFocus()) return;
910         NSString *s = [ aString isKindOfClass:[ NSAttributedString class ] ] ? [ aString string ] : (NSString *)aString;
912         const char *insert_point = NULL;
913         const char *replace_range = NULL;
914         if (replacementRange.location != NSNotFound) {
915                 /* Calculate the part to be replaced. */
916                 insert_point = Utf8AdvanceByUtf16Units(_focused_window->GetFocusedText(), replacementRange.location);
917                 replace_range = Utf8AdvanceByUtf16Units(insert_point, replacementRange.length);
918         }
920         HandleTextInput(NULL, true);
921         HandleTextInput([ s UTF8String ], false, NULL, insert_point, replace_range);
924 /** Insert the given text at the caret. */
925 - (void)insertText:(id)aString
927         [ self insertText:aString replacementRange:NSMakeRange(NSNotFound, 0) ];
930 /** Set a new marked text and reposition the caret. */
931 - (void)setMarkedText:(id)aString selectedRange:(NSRange)selRange replacementRange:(NSRange)replacementRange
933         if (!EditBoxInGlobalFocus()) return;
935         NSString *s = [ aString isKindOfClass:[ NSAttributedString class ] ] ? [ aString string ] : (NSString *)aString;
937         const char *utf8 = [ s UTF8String ];
938         if (utf8 != NULL) {
939                 const char *insert_point = NULL;
940                 const char *replace_range = NULL;
941                 if (replacementRange.location != NSNotFound) {
942                         /* Calculate the part to be replaced. */
943                         NSRange marked = [ self markedRange ];
944                         insert_point = Utf8AdvanceByUtf16Units(_focused_window->GetFocusedText(), replacementRange.location + (marked.location != NSNotFound ? marked.location : 0u));
945                         replace_range = Utf8AdvanceByUtf16Units(insert_point, replacementRange.length);
946                 }
948                 /* Convert caret index into a pointer in the UTF-8 string. */
949                 const char *selection = Utf8AdvanceByUtf16Units(utf8, selRange.location);
951                 HandleTextInput(utf8, true, selection, insert_point, replace_range);
952         }
955 /** Set a new marked text and reposition the caret. */
956 - (void)setMarkedText:(id)aString selectedRange:(NSRange)selRange
958         [ self setMarkedText:aString selectedRange:selRange replacementRange:NSMakeRange(NSNotFound, 0) ];
961 /** Unmark the current marked text. */
962 - (void)unmarkText
964         HandleTextInput(NULL, true);
967 /** Get the caret position. */
968 - (NSRange)selectedRange
970         if (!EditBoxInGlobalFocus()) return NSMakeRange(NSNotFound, 0);
972         NSUInteger start = CountUtf16Units(_focused_window->GetFocusedText(), _focused_window->GetCaret());
973         return NSMakeRange(start, 0);
976 /** Get the currently marked range. */
977 - (NSRange)markedRange
979         if (!EditBoxInGlobalFocus()) return NSMakeRange(NSNotFound, 0);
981         size_t mark_len;
982         const char *mark = _focused_window->GetMarkedText(&mark_len);
983         if (mark != NULL) {
984                 NSUInteger start = CountUtf16Units(_focused_window->GetFocusedText(), mark);
985                 NSUInteger len = CountUtf16Units(mark, mark + mark_len);
987                 return NSMakeRange(start, len);
988         }
990         return NSMakeRange(NSNotFound, 0);
993 /** Is any text marked? */
994 - (BOOL)hasMarkedText
996         if (!EditBoxInGlobalFocus()) return NO;
998         size_t len;
999         return _focused_window->GetMarkedText(&len) != NULL;
1002 /** Get a string corresponding to the given range. */
1003 - (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)theRange actualRange:(NSRangePointer)actualRange
1005         if (!EditBoxInGlobalFocus()) return nil;
1007         NSString *s = [ NSString stringWithUTF8String:_focused_window->GetFocusedText() ];
1008         NSRange valid_range = NSIntersectionRange(NSMakeRange(0, [ s length ]), theRange);
1010         if (actualRange != NULL) *actualRange = valid_range;
1011         if (valid_range.length == 0) return nil;
1013         return [ [ [ NSAttributedString alloc ] initWithString:[ s substringWithRange:valid_range ] ] autorelease ];
1016 /** Get a string corresponding to the given range. */
1017 - (NSAttributedString *)attributedSubstringFromRange:(NSRange)theRange
1019         return [ self attributedSubstringForProposedRange:theRange actualRange:NULL ];
1022 /** Get the current edit box string. */
1023 - (NSAttributedString *)attributedString
1025         if (!EditBoxInGlobalFocus()) return [ [ [ NSAttributedString alloc ] initWithString:@"" ] autorelease ];
1027         return [ [ [ NSAttributedString alloc ] initWithString:[ NSString stringWithUTF8String:_focused_window->GetFocusedText() ] ] autorelease ];
1030 /** Get the character that is rendered at the given point. */
1031 - (NSUInteger)characterIndexForPoint:(NSPoint)thePoint
1033         if (!EditBoxInGlobalFocus()) return NSNotFound;
1035         NSPoint view_pt = [ self convertPoint:[ [ self window ] convertScreenToBase:thePoint ] fromView:nil ];
1037         Point pt = { (int)view_pt.x, (int)[ self frame ].size.height - (int)view_pt.y };
1039         const char *ch = _focused_window->GetTextCharacterAtPosition(pt);
1040         if (ch == NULL) return NSNotFound;
1042         return CountUtf16Units(_focused_window->GetFocusedText(), ch);
1045 /** Get the bounding rect for the given range. */
1046 - (NSRect)firstRectForCharacterRange:(NSRange)aRange
1048         if (!EditBoxInGlobalFocus()) return NSMakeRect(0, 0, 0, 0);
1050         /* Convert range to UTF-8 string pointers. */
1051         const char *start = Utf8AdvanceByUtf16Units(_focused_window->GetFocusedText(), aRange.location);
1052         const char *end = aRange.length != 0 ? Utf8AdvanceByUtf16Units(_focused_window->GetFocusedText(), aRange.location + aRange.length) : start;
1054         /* Get the bounding rect for the text range.*/
1055         Rect r = _focused_window->GetTextBoundingRect(start, end);
1056         NSRect view_rect = NSMakeRect(_focused_window->left + r.left, [ self frame ].size.height - _focused_window->top - r.bottom, r.right - r.left, r.bottom - r.top);
1058 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_7
1059         if ([ [ self window ] respondsToSelector:@selector(convertRectToScreen:) ]) {
1060                 return [ [ self window ] convertRectToScreen:[ self convertRect:view_rect toView:nil ] ];
1061         }
1062 #endif
1064         NSRect window_rect = [ self convertRect:view_rect toView:nil ];
1065         NSPoint origin = [ [ self window ] convertBaseToScreen:window_rect.origin ];
1066         return NSMakeRect(origin.x, origin.y, window_rect.size.width, window_rect.size.height);
1069 /** Get the bounding rect for the given range. */
1070 - (NSRect)firstRectForCharacterRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange
1072         return [ self firstRectForCharacterRange:aRange ];
1075 /** Get all string attributes that we can process for marked text. */
1076 - (NSArray*)validAttributesForMarkedText
1078         return [ NSArray array ];
1081 /** Identifier for this text input instance. */
1082 #if MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_5
1083 - (long)conversationIdentifier
1084 #else
1085 - (NSInteger)conversationIdentifier
1086 #endif
1088         return 0;
1091 /** Delete single character left of the cursor. */
1092 - (void)deleteBackward:(id)sender
1094         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_BACKSPACE, 0);
1097 /** Delete word left of the cursor. */
1098 - (void)deleteWordBackward:(id)sender
1100         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_BACKSPACE | WKC_CTRL, 0);
1103 /** Delete single character right of the cursor. */
1104 - (void)deleteForward:(id)sender
1106         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_DELETE, 0);
1109 /** Delete word right of the cursor. */
1110 - (void)deleteWordForward:(id)sender
1112         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_DELETE | WKC_CTRL, 0);
1115 /** Move cursor one character left. */
1116 - (void)moveLeft:(id)sender
1118         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_LEFT, 0);
1121 /** Move cursor one word left. */
1122 - (void)moveWordLeft:(id)sender
1124         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_LEFT | WKC_CTRL, 0);
1127 /** Move cursor one character right. */
1128 - (void)moveRight:(id)sender
1130         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_RIGHT, 0);
1133 /** Move cursor one word right. */
1134 - (void)moveWordRight:(id)sender
1136         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_RIGHT | WKC_CTRL, 0);
1139 /** Move cursor one line up. */
1140 - (void)moveUp:(id)sender
1142         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_UP, 0);
1145 /** Move cursor one line down. */
1146 - (void)moveDown:(id)sender
1148         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_DOWN, 0);
1151 /** MScroll one line up. */
1152 - (void)moveUpAndModifySelection:(id)sender
1154         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_UP | WKC_SHIFT, 0);
1157 /** Scroll one line down. */
1158 - (void)moveDownAndModifySelection:(id)sender
1160         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_DOWN | WKC_SHIFT, 0);
1163 /** Move cursor to the start of the line. */
1164 - (void)moveToBeginningOfLine:(id)sender
1166         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_HOME, 0);
1169 /** Move cursor to the end of the line. */
1170 - (void)moveToEndOfLine:(id)sender
1172         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_END, 0);
1175 /** Scroll one page up. */
1176 - (void)scrollPageUp:(id)sender
1178         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_PAGEUP, 0);
1181 /** Scroll one page down. */
1182 - (void)scrollPageDown:(id)sender
1184         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_PAGEDOWN, 0);
1187 /** Move cursor (and selection) one page up. */
1188 - (void)pageUpAndModifySelection:(id)sender
1190         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_PAGEUP | WKC_SHIFT, 0);
1193 /** Move cursor (and selection) one page down. */
1194 - (void)pageDownAndModifySelection:(id)sender
1196         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_PAGEDOWN | WKC_SHIFT, 0);
1199 /** Scroll to the beginning of the document. */
1200 - (void)scrollToBeginningOfDocument:(id)sender
1202         /* For compatibility with OTTD on Win/Linux. */
1203         [ self moveToBeginningOfLine:sender ];
1206 /** Scroll to the end of the document. */
1207 - (void)scrollToEndOfDocument:(id)sender
1209         /* For compatibility with OTTD on Win/Linux. */
1210         [ self moveToEndOfLine:sender ];
1213 /** Return was pressed. */
1214 - (void)insertNewline:(id)sender
1216         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_RETURN, '\r');
1219 /** Escape was pressed. */
1220 - (void)cancelOperation:(id)sender
1222         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_ESC, 0);
1225 /** Invoke the selector if we implement it. */
1226 - (void)doCommandBySelector:(SEL)aSelector
1228         if ([ self respondsToSelector:aSelector ]) [ self performSelector:aSelector ];
1231 @end
1235 @implementation OTTD_CocoaWindowDelegate
1236 /** Initialize the video driver */
1237 - (void)setDriver:(CocoaSubdriver*)drv
1239         driver = drv;
1241 /** Handle closure requests */
1242 - (BOOL)windowShouldClose:(id)sender
1244         HandleExitGameRequest();
1246         return NO;
1248 /** Handle key acceptance */
1249 - (void)windowDidBecomeKey:(NSNotification*)aNotification
1251         driver->active = true;
1253 /** Resign key acceptance */
1254 - (void)windowDidResignKey:(NSNotification*)aNotification
1256         driver->active = false;
1258 /** Handle becoming main window */
1259 - (void)windowDidBecomeMain:(NSNotification*)aNotification
1261         driver->active = true;
1263 /** Resign being main window */
1264 - (void)windowDidResignMain:(NSNotification*)aNotification
1266         driver->active = false;
1268 /** Window entered fullscreen mode (10.7). */
1269 - (void)windowDidEnterFullScreen:(NSNotification *)aNotification
1271         NSPoint loc = [ driver->cocoaview convertPoint:[ [ aNotification object ] mouseLocationOutsideOfEventStream ] fromView:nil ];
1272         BOOL inside = ([ driver->cocoaview hitTest:loc ] == driver->cocoaview);
1274         if (inside) [ driver->cocoaview mouseEntered:NULL ];
1277 @end
1279 #endif /* WITH_COCOA */