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/>.
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 ******************************************************************************/
20 #include "../../stdafx.h"
21 #include "../../os/macosx/macos.h"
24 #define Point OTTDPoint
25 #import <Cocoa/Cocoa.h>
29 #include "../../openttd.h"
30 #include "../../debug.h"
31 #include "../../core/geometry_type.hpp"
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 */
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.
50 @interface OTTDMain : NSObject
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";
65 * The main class of the application, the application's delegate.
67 @implementation OTTDMain
69 * Stop the game engine. Must be called on main thread.
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 ];
81 * Start the game loop.
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. */
91 /* We are done, thank you for playing. */
92 [ self performSelectorOnMainThread:@selector(stopEngine) withObject:nil waitUntilDone:FALSE ];
96 * Called when the internal event loop has just started running.
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.
110 - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*) sender
112 HandleExitGameRequest();
114 return NSTerminateCancel; // NSTerminateLater ?
118 * Remove ourself as a notification observer.
120 - (void)unregisterObserver
122 [ [ NSNotificationCenter defaultCenter ] removeObserver:self ];
127 * Initialize the application menu shown in top bar.
129 static void setApplicationMenu()
131 NSString *appName = @"OpenTTD";
132 NSMenu *appleMenu = [ [ NSMenu alloc ] initWithTitle:appName ];
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.
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" ];
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.
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);
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();
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;
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 */
245 for (CFIndex i = 0; i < num_modes && count < max_modes; i++) {
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) {
272 if (hasMode) continue;
274 /* Add mode to the list */
275 modes[count].x = width;
276 modes[count].y = height;
280 /* Sort list smallest to largest */
281 QSortT(modes, count, &ModeSorter);
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)
292 if (MacOSVersionIsAtLeast(10, 5, 0)) return false;
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
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;
319 _num_resolutions = count;
323 * Handle a change of the display area.
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();
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.
349 static CocoaSubdriver *QZ_CreateWindowSubdriver(int width, int height, int bpp)
351 #if defined(ENABLE_COCOA_QUARTZ) || defined(ENABLE_COCOA_QUICKDRAW)
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;
363 #ifdef ENABLE_COCOA_QUICKDRAW
364 ret = QZ_CreateWindowQuickdrawSubdriver(width, height, bpp);
365 if (ret != NULL) return ret;
368 #if defined(ENABLE_COCOA_QUARTZ) && (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4)
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.
373 if (MacOSVersionIsAtLeast(10, 4, 0)) {
374 ret = QZ_CreateWindowQuartzSubdriver(width, height, bpp);
375 if (ret != NULL) return ret;
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.
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();
400 #if (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_9)
402 ret = fullscreen ? QZ_CreateFullscreenSubdriver(width, height, bpp) : QZ_CreateWindowSubdriver(width, height, bpp);
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;
422 #endif /* defined(_DEBUG) && (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_9) */
428 static FVideoDriver_Cocoa iFVideoDriver_Cocoa;
431 * Stop the cocoa video subdriver.
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.
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;
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) {
469 return "Could not create subdriver";
472 QZ_GameSizeChanged();
473 QZ_UpdateVideoModes();
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.
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.
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. */
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.
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();
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.
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");
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.
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.
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) ];
579 [ [ NSInputManager currentInputManager ] markedTextAbandoned:_cocoa_subdriver->cocoaview ];
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 .
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);
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.
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);
627 _searchpaths[SP_APPLICATION_BUNDLE_DIR] = NULL;
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
639 void cocoaSetupAutoreleasePool()
641 _ottd_autorelease_pool = [ [ NSAutoreleasePool alloc ] init ];
645 * Autorelease the application pool.
647 void cocoaReleaseAutoreleasePool()
649 [ _ottd_autorelease_pool release ];
654 * Re-implement the system cursor in order to allow hiding and showing it nicely
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) ];
674 @implementation OTTD_CocoaWindow
676 - (void)setDriver:(CocoaSubdriver*)drv
681 * Minimize the window
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.
702 driver->SetPortAlphaOpaque();
704 /* save current visible surface */
705 [ self cacheImageInRect:[ driver->cocoaview frame ] ];
707 /* let the window manager redraw controls, border, etc */
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
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
731 - (void)appDidHide:(NSNotification*)note
733 driver->active = false;
736 * Fade-in the application and restore display plane
738 - (void)appWillUnhide:(NSNotification*)note
740 driver->SetPortAlphaOpaque ();
743 * Unhide and restore display plane and re-activate driver
745 - (void)appDidUnhide:(NSNotification*)note
747 driver->active = true;
750 * Initialize event system for the application rectangle
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 ];
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.
777 static NSUInteger CountUtf16Units(const char *from, const char *to)
783 size_t len = Utf8Decode(&c, from);
784 i += len < 4 ? 1 : 2; // Watch for surrogate pairs.
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.
797 static const char *Utf8AdvanceByUtf16Units(const char *str, NSUInteger count)
799 for (NSUInteger i = 0; i < count && *str != '\0'; ) {
801 size_t len = Utf8Decode(&c, str);
802 i += len < 4 ? 1 : 2; // Watch for surrogates.
809 @implementation OTTD_CocoaView
811 * Initialize the driver
813 - (void)setDriver:(CocoaSubdriver*)drv
818 * Define the opaqueness of the window / screen
819 * @return opaqueness of window / screen
826 * Draws a rectangle on the screen.
827 * It's overwritten by the individual drivers but must be defined
829 - (void)drawRect:(NSRect)invalidRect
834 * Allow to handle events
836 - (BOOL)acceptsFirstResponder
841 * Actually handle events
843 - (BOOL)becomeFirstResponder
848 * Define the rectangle where we draw our application window
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
860 - (void)clearTrackingRect
862 [ self removeTrackingRect:trackingtag ];
865 * Declare responsibility for the cursor within our application rect
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
877 - (void)viewWillMoveToWindow:(NSWindow *)win
879 if (!win && [ self window ]) [ self clearTrackingRect ];
882 * Restore our responsibility for our application window after moving
884 - (void)viewDidMoveToWindow
886 if ([ self window ]) [ self setTrackingRect ];
889 * Make OpenTTD aware that it has control over the mouse
891 - (void)mouseEntered:(NSEvent *)theEvent
893 _cursor.in_window = true;
896 * Make OpenTTD aware that it has NOT control over the mouse
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);
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 ];
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);
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);
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. */
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);
982 const char *mark = _focused_window->GetMarkedText(&mark_len);
984 NSUInteger start = CountUtf16Units(_focused_window->GetFocusedText(), mark);
985 NSUInteger len = CountUtf16Units(mark, mark + mark_len);
987 return NSMakeRange(start, len);
990 return NSMakeRange(NSNotFound, 0);
993 /** Is any text marked? */
994 - (BOOL)hasMarkedText
996 if (!EditBoxInGlobalFocus()) return NO;
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 ] ];
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
1085 - (NSInteger)conversationIdentifier
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 ];
1235 @implementation OTTD_CocoaWindowDelegate
1236 /** Initialize the video driver */
1237 - (void)setDriver:(CocoaSubdriver*)drv
1241 /** Handle closure requests */
1242 - (BOOL)windowShouldClose:(id)sender
1244 HandleExitGameRequest();
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 ];
1279 #endif /* WITH_COCOA */