/* $Id$ */ /****************************************************************************** * Cocoa video driver * * Known things left to do: * * Nothing at the moment. * ******************************************************************************/ #ifdef WITH_COCOA #import #import /* gettimeofday */ #import /* for MAXPATHLEN */ #import /* Portions of CPS.h */ typedef struct CPSProcessSerNum { UInt32 lo; UInt32 hi; } CPSProcessSerNum; extern OSErr CPSGetCurrentProcess(CPSProcessSerNum* psn); extern OSErr CPSEnableForegroundOperation(CPSProcessSerNum* psn, UInt32 _arg2, UInt32 _arg3, UInt32 _arg4, UInt32 _arg5); extern OSErr CPSSetFrontProcess(CPSProcessSerNum* psn); /* From Menus.h (according to Xcode Developer Documentation) */ extern void ShowMenuBar(void); extern void HideMenuBar(void); /* Disables a warning. This is needed since the method exists but has been dropped from the header, supposedly as of 10.4. */ #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4) @interface NSApplication(NSAppleMenu) - (void)setAppleMenu:(NSMenu *)menu; @end #endif /* Defined in ppc/param.h or i386/param.h included from sys/param.h */ #undef ALIGN /* Defined in stdbool.h */ #ifndef __cplusplus # ifndef __BEOS__ # undef bool # undef false # undef true # endif #endif #include "../stdafx.h" #include "../openttd.h" #include "../debug.h" #include "../functions.h" #include "../gfx.h" #include "../macros.h" #include "../sdl.h" #include "../window.h" #include "../network.h" #include "../variables.h" #include "../os/macosx/splash.h" #include "cocoa_v.h" #include "cocoa_keys.h" #undef Point #undef Rect /* Subclass of NSWindow to fix genie effect and support resize events */ @interface OTTD_QuartzWindow : NSWindow - (void)miniaturize:(id)sender; - (void)display; - (void)setFrame:(NSRect)frameRect display:(BOOL)flag; - (void)appDidHide:(NSNotification*)note; - (void)appWillUnhide:(NSNotification*)note; - (void)appDidUnhide:(NSNotification*)note; - (id)initWithContentRect:(NSRect)contentRect styleMask:(unsigned int)styleMask backing:(NSBackingStoreType)backingType defer:(BOOL)flag; @end /* Delegate for our NSWindow to send ask for quit on close */ @interface OTTD_QuartzWindowDelegate : NSObject - (BOOL)windowShouldClose:(id)sender; @end @interface OTTDMain : NSObject @end /* Structure for rez switch gamma fades * We can hide the monitor flicker by setting the gamma tables to 0 */ #define QZ_GAMMA_TABLE_SIZE 256 typedef struct { CGGammaValue red[QZ_GAMMA_TABLE_SIZE]; CGGammaValue green[QZ_GAMMA_TABLE_SIZE]; CGGammaValue blue[QZ_GAMMA_TABLE_SIZE]; } OTTD_QuartzGammaTable; /* Add methods to get at private members of NSScreen. * Since there is a bug in Apple's screen switching code that does not update * this variable when switching to fullscreen, we'll set it manually (but only * for the main screen). */ @interface NSScreen (NSScreenAccess) - (void) setFrame:(NSRect)frame; @end @implementation NSScreen (NSScreenAccess) - (void) setFrame:(NSRect)frame; { _frame = frame; } @end static void QZ_Draw(void); static void QZ_UnsetVideoMode(void); static void QZ_UpdatePalette(uint start, uint count); static void QZ_WarpCursor(int x, int y); static void QZ_ShowMouse(void); static void QZ_HideMouse(void); static void CocoaVideoFullScreen(bool full_screen); static NSAutoreleasePool *_ottd_autorelease_pool; static OTTDMain *_ottd_main; static struct CocoaVideoData { bool isset; bool issetting; CGDirectDisplayID display_id; /* 0 == main display (only support single display) */ CFDictionaryRef mode; /* current mode of the display */ CFDictionaryRef save_mode; /* original mode of the display */ CFArrayRef mode_list; /* list of available fullscreen modes */ CGDirectPaletteRef palette; /* palette of an 8-bit display */ uint32 device_width; uint32 device_height; uint32 device_bpp; void *realpixels; uint8 *pixels; uint32 width; uint32 height; uint32 pitch; bool fullscreen; unsigned int current_mods; bool tab_is_down; bool emulating_right_button; bool cursor_visible; bool active; #ifdef _DEBUG uint32 tEvent; #endif OTTD_QuartzWindow *window; NSQuickDrawView *qdview; #define MAX_DIRTY_RECTS 100 OTTDRect dirty_rects[MAX_DIRTY_RECTS]; int num_dirty_rects; uint16 palette16[256]; uint32 palette32[256]; } _cocoa_video_data; static bool _cocoa_video_started = false; static bool _cocoa_video_dialog = false; /****************************************************************************** * Game loop and accessories * ******************************************************************************/ static uint32 GetTick(void) { struct timeval tim; gettimeofday(&tim, NULL); return tim.tv_usec / 1000 + tim.tv_sec * 1000; } static void QZ_CheckPaletteAnim(void) { if (_pal_last_dirty != -1) { QZ_UpdatePalette(_pal_first_dirty, _pal_last_dirty - _pal_first_dirty + 1); _pal_last_dirty = -1; } } extern void DoExitSave(void); static void QZ_AskQuit(void) { if (_game_mode == GM_MENU) { // do not ask to quit on the main screen _exit_game = true; } else if (_patches.autosave_on_exit) { DoExitSave(); _exit_game = true; } else { AskExitGame(); } } typedef struct VkMapping { unsigned short vk_from; byte map_to; } VkMapping; #define AS(x, z) {x, z} static const VkMapping _vk_mapping[] = { AS(QZ_BACKQUOTE, WKC_BACKQUOTE), // key left of '1' AS(QZ_BACKQUOTE2, WKC_BACKQUOTE), // some keyboards have it on another scancode // Pageup stuff + up/down //AM(SDLK_PAGEUP, SDLK_PAGEDOWN, WKC_PAGEUP, WKC_PAGEDOWN), <==== Does this include HOME/END? AS(QZ_PAGEUP, WKC_PAGEUP), AS(QZ_PAGEDOWN, WKC_PAGEDOWN), AS(QZ_UP, WKC_UP), AS(QZ_DOWN, WKC_DOWN), AS(QZ_LEFT, WKC_LEFT), AS(QZ_RIGHT, WKC_RIGHT), AS(QZ_HOME, WKC_HOME), AS(QZ_END, WKC_END), AS(QZ_INSERT, WKC_INSERT), AS(QZ_DELETE, WKC_DELETE), // Letters. QZ_[a-z] is not in numerical order so we can't use AM(...) AS(QZ_a, 'A'), AS(QZ_b, 'B'), AS(QZ_c, 'C'), AS(QZ_d, 'D'), AS(QZ_e, 'E'), AS(QZ_f, 'F'), AS(QZ_g, 'G'), AS(QZ_h, 'H'), AS(QZ_i, 'I'), AS(QZ_j, 'J'), AS(QZ_k, 'K'), AS(QZ_l, 'L'), AS(QZ_m, 'M'), AS(QZ_n, 'N'), AS(QZ_o, 'O'), AS(QZ_p, 'P'), AS(QZ_q, 'Q'), AS(QZ_r, 'R'), AS(QZ_s, 'S'), AS(QZ_t, 'T'), AS(QZ_u, 'U'), AS(QZ_v, 'V'), AS(QZ_w, 'W'), AS(QZ_x, 'X'), AS(QZ_y, 'Y'), AS(QZ_z, 'Z'), // Same thing for digits AS(QZ_0, '0'), AS(QZ_1, '1'), AS(QZ_2, '2'), AS(QZ_3, '3'), AS(QZ_4, '4'), AS(QZ_5, '5'), AS(QZ_6, '6'), AS(QZ_7, '7'), AS(QZ_8, '8'), AS(QZ_9, '9'), AS(QZ_ESCAPE, WKC_ESC), AS(QZ_PAUSE, WKC_PAUSE), AS(QZ_BACKSPACE, WKC_BACKSPACE), AS(QZ_SPACE, WKC_SPACE), AS(QZ_RETURN, WKC_RETURN), AS(QZ_TAB, WKC_TAB), // Function keys AS(QZ_F1, WKC_F1), AS(QZ_F2, WKC_F2), AS(QZ_F3, WKC_F3), AS(QZ_F4, WKC_F4), AS(QZ_F5, WKC_F5), AS(QZ_F6, WKC_F6), AS(QZ_F7, WKC_F7), AS(QZ_F8, WKC_F8), AS(QZ_F9, WKC_F9), AS(QZ_F10, WKC_F10), AS(QZ_F11, WKC_F11), AS(QZ_F12, WKC_F12), // Numeric part. AS(QZ_KP0, WKC_NUM_0), AS(QZ_KP1, WKC_NUM_1), AS(QZ_KP2, WKC_NUM_2), AS(QZ_KP3, WKC_NUM_3), AS(QZ_KP4, WKC_NUM_4), AS(QZ_KP5, WKC_NUM_5), AS(QZ_KP6, WKC_NUM_6), AS(QZ_KP7, WKC_NUM_7), AS(QZ_KP8, WKC_NUM_8), AS(QZ_KP9, WKC_NUM_9), AS(QZ_KP_DIVIDE, WKC_NUM_DIV), AS(QZ_KP_MULTIPLY, WKC_NUM_MUL), AS(QZ_KP_MINUS, WKC_NUM_MINUS), AS(QZ_KP_PLUS, WKC_NUM_PLUS), AS(QZ_KP_ENTER, WKC_NUM_ENTER), AS(QZ_KP_PERIOD, WKC_NUM_DECIMAL) }; static uint32 QZ_MapKey(unsigned short sym) { const VkMapping *map; uint32 key = 0; for (map = _vk_mapping; map != endof(_vk_mapping); ++map) { if (sym == map->vk_from) { key = map->map_to; break; } } if (_cocoa_video_data.current_mods & NSShiftKeyMask) key |= WKC_SHIFT; if (_cocoa_video_data.current_mods & NSControlKeyMask) key |= WKC_CTRL; if (_cocoa_video_data.current_mods & NSAlternateKeyMask) key |= WKC_ALT; if (_cocoa_video_data.current_mods & NSCommandKeyMask) key |= WKC_META; return key << 16; } static void QZ_KeyEvent(unsigned short keycode, unsigned short unicode, BOOL down) { switch (keycode) { case QZ_UP: SB(_dirkeys, 1, 1, down); break; case QZ_DOWN: SB(_dirkeys, 3, 1, down); break; case QZ_LEFT: SB(_dirkeys, 0, 1, down); break; case QZ_RIGHT: SB(_dirkeys, 2, 1, down); break; case QZ_TAB: _cocoa_video_data.tab_is_down = down; break; case QZ_RETURN: case QZ_f: if (down && ( (_cocoa_video_data.current_mods & NSControlKeyMask) || (_cocoa_video_data.current_mods & NSCommandKeyMask) )) { CocoaVideoFullScreen(!_fullscreen); } break; } if (down) { _pressed_key = QZ_MapKey(keycode) | unicode; DEBUG(driver, 2)("cocoa_v: QZ_KeyEvent: %x (%x), down, mapping: %x", keycode, unicode, _pressed_key); } else { DEBUG(driver, 2)("cocoa_v: QZ_KeyEvent: %x (%x), up", keycode, unicode); } } static void QZ_DoUnsidedModifiers(unsigned int newMods) { const int mapping[] = { QZ_CAPSLOCK, QZ_LSHIFT, QZ_LCTRL, QZ_LALT, QZ_LMETA }; int i; int bit; if (_cocoa_video_data.current_mods == newMods) return; /* Iterate through the bits, testing each against the current modifiers */ for (i = 0, bit = NSAlphaShiftKeyMask; bit <= NSCommandKeyMask; bit <<= 1, ++i) { unsigned int currentMask, newMask; currentMask = _cocoa_video_data.current_mods & bit; newMask = newMods & bit; if (currentMask && currentMask != newMask) { /* modifier up event */ /* If this was Caps Lock, we need some additional voodoo to make SDL happy (is this needed in ottd?) */ if (bit == NSAlphaShiftKeyMask) QZ_KeyEvent(mapping[i], 0, YES); QZ_KeyEvent(mapping[i], 0, NO); } else if (newMask && currentMask != newMask) { /* modifier down event */ QZ_KeyEvent(mapping[i], 0, YES); /* If this was Caps Lock, we need some additional voodoo to make SDL happy (is this needed in ottd?) */ if (bit == NSAlphaShiftKeyMask) QZ_KeyEvent(mapping[i], 0, NO); } } _cocoa_video_data.current_mods = newMods; } static void QZ_MouseMovedEvent(int x, int y) { if (_cursor.fix_at) { int dx = x - _cursor.pos.x; int dy = y - _cursor.pos.y; if (dx != 0 || dy != 0) { _cursor.delta.x += dx; _cursor.delta.y += dy; QZ_WarpCursor(_cursor.pos.x, _cursor.pos.y); } } else { _cursor.delta.x = x - _cursor.pos.x; _cursor.delta.y = y - _cursor.pos.y; _cursor.pos.x = x; _cursor.pos.y = y; _cursor.dirty = true; } } void QZ_MouseButtonEvent(int button, BOOL down) { switch (button) { case 0: if (down) { _left_button_down = true; } else { _left_button_down = false; _left_button_clicked = false; } break; case 1: if (down) { _right_button_down = true; _right_button_clicked = true; } else { _right_button_down = false; } break; } } static inline NSPoint QZ_GetMouseLocation(NSEvent *event) { NSPoint pt; if (_cocoa_video_data.fullscreen) { pt = [ NSEvent mouseLocation ]; pt.y = _cocoa_video_data.height - pt.y; } else { pt = [event locationInWindow]; pt = [_cocoa_video_data.qdview convertPoint:pt fromView:nil]; } return pt; } static bool QZ_MouseIsInsideView(NSPoint *pt) { if (_cocoa_video_data.fullscreen) { return pt->x >= 0 && pt->y >= 0 && pt->x < _cocoa_video_data.width && pt->y < _cocoa_video_data.height; } else { return [ _cocoa_video_data.qdview mouse:*pt inRect:[ _cocoa_video_data.qdview bounds ] ]; } } static bool QZ_PollEvent(void) { NSEvent *event; NSPoint pt; NSString *chars; #ifdef _DEBUG uint32 et0, et; #endif #ifdef _DEBUG et0 = GetTick(); #endif event = [ NSApp nextEventMatchingMask:NSAnyEventMask untilDate: [ NSDate distantPast ] inMode: NSDefaultRunLoopMode dequeue:YES ]; #ifdef _DEBUG et = GetTick(); _cocoa_video_data.tEvent+= et - et0; #endif if (event == nil) return false; if (!_cocoa_video_data.active) { QZ_ShowMouse(); [NSApp sendEvent:event]; return true; } QZ_DoUnsidedModifiers( [ event modifierFlags ] ); switch ([event type]) { case NSMouseMoved: case NSOtherMouseDragged: case NSRightMouseDragged: case NSLeftMouseDragged: pt = QZ_GetMouseLocation(event); if (!QZ_MouseIsInsideView(&pt) && !_cocoa_video_data.emulating_right_button) { QZ_ShowMouse(); [NSApp sendEvent:event]; break; } QZ_HideMouse(); QZ_MouseMovedEvent((int)pt.x, (int)pt.y); break; case NSLeftMouseDown: if (!([ event modifierFlags ] & NSCommandKeyMask) || !QZ_MouseIsInsideView(&pt)) { [NSApp sendEvent:event]; } pt = QZ_GetMouseLocation(event); if (!QZ_MouseIsInsideView(&pt)) { QZ_ShowMouse(); break; } QZ_HideMouse(); QZ_MouseMovedEvent((int)pt.x, (int)pt.y); /* Right mouse button emulation */ if ([ event modifierFlags ] & NSCommandKeyMask) { _cocoa_video_data.emulating_right_button = true; QZ_MouseButtonEvent(1, YES); } else { QZ_MouseButtonEvent(0, YES); } break; case NSLeftMouseUp: [NSApp sendEvent:event]; pt = QZ_GetMouseLocation(event); if (!QZ_MouseIsInsideView(&pt)) { QZ_ShowMouse(); break; } QZ_HideMouse(); QZ_MouseMovedEvent((int)pt.x, (int)pt.y); /* Right mouse button emulation */ if (_cocoa_video_data.emulating_right_button) { _cocoa_video_data.emulating_right_button = false; QZ_MouseButtonEvent(1, NO); } else { QZ_MouseButtonEvent(0, NO); } break; case NSRightMouseDown: pt = QZ_GetMouseLocation(event); if (!QZ_MouseIsInsideView(&pt)) { QZ_ShowMouse(); [NSApp sendEvent:event]; break; } QZ_HideMouse(); QZ_MouseMovedEvent((int)pt.x, (int)pt.y); QZ_MouseButtonEvent(1, YES); break; case NSRightMouseUp: pt = QZ_GetMouseLocation(event); if (!QZ_MouseIsInsideView(&pt)) { QZ_ShowMouse(); [NSApp sendEvent:event]; break; } QZ_HideMouse(); QZ_MouseMovedEvent((int)pt.x, (int)pt.y); QZ_MouseButtonEvent(1, NO); break; #if 0 /* This is not needed since openttd currently only use two buttons */ case NSOtherMouseDown: pt = QZ_GetMouseLocation(event); if (!QZ_MouseIsInsideView(&pt)) { QZ_ShowMouse(); [NSApp sendEvent:event]; break; } QZ_HideMouse(); QZ_MouseMovedEvent((int)pt.x, (int)pt.y); QZ_MouseButtonEvent([ event buttonNumber ], YES); break; case NSOtherMouseUp: pt = QZ_GetMouseLocation(event); if (!QZ_MouseIsInsideView(&pt)) { QZ_ShowMouse(); [NSApp sendEvent:event]; break; } QZ_HideMouse(); QZ_MouseMovedEvent((int)pt.x, (int)pt.y); QZ_MouseButtonEvent([ event buttonNumber ], NO); break; #endif case NSKeyDown: /* Quit, hide and minimize */ switch ([event keyCode]) { case QZ_q: case QZ_h: case QZ_m: if ([ event modifierFlags ] & NSCommandKeyMask) { [NSApp sendEvent:event]; } break; } chars = [ event characters ]; QZ_KeyEvent([event keyCode], [ chars length ] ? [ chars characterAtIndex:0 ] : 0, YES); break; case NSKeyUp: /* Quit, hide and minimize */ switch ([event keyCode]) { case QZ_q: case QZ_h: case QZ_m: if ([ event modifierFlags ] & NSCommandKeyMask) { [NSApp sendEvent:event]; } break; } chars = [ event characters ]; QZ_KeyEvent([event keyCode], [ chars length ] ? [ chars characterAtIndex:0 ] : 0, NO); break; case NSScrollWheel: if ([ event deltaX ] > 0.0 || [ event deltaY ] > 0.0) { /* Scroll up */ _cursor.wheel--; } else { /* Scroll down */ _cursor.wheel++; } break; default: [NSApp sendEvent:event]; } return true; } static void QZ_GameLoop(void) { uint32 next_tick = GetTick() + 30; uint32 cur_ticks; uint32 pal_tick = 0; #ifdef _DEBUG uint32 et0, et, st0, st; #endif int i; DEBUG(driver, 1)("cocoa_v: QZ_GameLoop"); #ifdef _DEBUG et0 = GetTick(); st = 0; #endif _screen.dst_ptr = _cocoa_video_data.pixels; DisplaySplashImage(); QZ_CheckPaletteAnim(); QZ_Draw(); CSleep(1); for (i = 0; i < 2; i++) GameLoop(); _screen.dst_ptr = _cocoa_video_data.pixels; UpdateWindows(); QZ_CheckPaletteAnim(); QZ_Draw(); CSleep(1); for (;;) { InteractiveRandom(); // randomness while (QZ_PollEvent()) {} if (_exit_game) break; #if defined(_DEBUG) if (_cocoa_video_data.current_mods & NSShiftKeyMask) #else if (_cocoa_video_data.tab_is_down) #endif { if (!_networking && _game_mode != GM_MENU) _fast_forward |= 2; } else if (_fast_forward & 2) { _fast_forward = 0; } cur_ticks = GetTick(); if ((_fast_forward && !_pause) || cur_ticks > next_tick) next_tick = cur_ticks; if (cur_ticks == next_tick) { next_tick += 30; _ctrl_pressed = !!(_cocoa_video_data.current_mods & NSControlKeyMask); _shift_pressed = !!(_cocoa_video_data.current_mods & NSShiftKeyMask); #ifdef _DEBUG _dbg_screen_rect = !!(_cocoa_video_data.current_mods & NSAlphaShiftKeyMask); #endif GameLoop(); _screen.dst_ptr = _cocoa_video_data.pixels; UpdateWindows(); if (++pal_tick > 4) { QZ_CheckPaletteAnim(); pal_tick = 1; } QZ_Draw(); } else { #ifdef _DEBUG st0 = GetTick(); #endif CSleep(1); #ifdef _DEBUG st += GetTick() - st0; #endif _screen.dst_ptr = _cocoa_video_data.pixels; DrawTextMessage(); DrawMouseCursor(); QZ_Draw(); } } #ifdef _DEBUG et = GetTick(); DEBUG(driver, 1)("cocoa_v: nextEventMatchingMask took %i ms total", _cocoa_video_data.tEvent); DEBUG(driver, 1)("cocoa_v: game loop took %i ms total (%i ms without sleep)", et - et0, et - et0 - st); DEBUG(driver, 1)("cocoa_v: (nextEventMatchingMask total)/(game loop total) is %f%%", (double)_cocoa_video_data.tEvent / (double)(et - et0) * 100); DEBUG(driver, 1)("cocoa_v: (nextEventMatchingMask total)/(game loop without sleep total) is %f%%", (double)_cocoa_video_data.tEvent / (double)(et - et0 - st) * 100); #endif } /****************************************************************************** * Windowed mode * ******************************************************************************/ /* This function makes the *game region* of the window 100% opaque. * The genie effect uses the alpha component. Otherwise, * it doesn't seem to matter what value it has. */ static void QZ_SetPortAlphaOpaque(void) { if (_cocoa_video_data.device_bpp == 32) { uint32* pixels = (uint32*)_cocoa_video_data.realpixels; uint32 rowPixels = _cocoa_video_data.pitch / 4; uint32 i; uint32 j; for (i = 0; i < _cocoa_video_data.height; i++) for (j = 0; j < _cocoa_video_data.width; j++) { pixels[i * rowPixels + j] |= 0xFF000000; } } } @implementation OTTD_QuartzWindow /* we override these methods to fix the miniaturize animation/dock icon bug */ - (void)miniaturize:(id)sender { /* make the alpha channel opaque so anim won't have holes in it */ QZ_SetPortAlphaOpaque (); /* window is hidden now */ _cocoa_video_data.active = false; QZ_ShowMouse(); [ super miniaturize:sender ]; } - (void)display { /* This method fires just before the window deminaturizes from the Dock. * We'll save the current visible surface, let the window manager redraw any * UI elements, and restore the surface. This way, no expose event * is required, and the deminiaturize works perfectly. */ QZ_SetPortAlphaOpaque(); /* save current visible surface */ [ self cacheImageInRect:[ _cocoa_video_data.qdview frame ] ]; /* let the window manager redraw controls, border, etc */ [ super display ]; /* restore visible surface */ [ self restoreCachedImage ]; /* window is visible again */ _cocoa_video_data.active = true; } - (void)setFrame:(NSRect)frameRect display:(BOOL)flag { NSRect newViewFrame; CGrafPtr thePort; [ super setFrame:frameRect display:flag ]; /* Don't do anything if the window is currently beign created */ if (_cocoa_video_data.issetting) return; if (_cocoa_video_data.window == nil) return; newViewFrame = [ _cocoa_video_data.qdview frame ]; /* Update the pixels and pitch */ thePort = [ _cocoa_video_data.qdview qdPort ]; LockPortBits(thePort); _cocoa_video_data.realpixels = GetPixBaseAddr(GetPortPixMap(thePort)); _cocoa_video_data.pitch = GetPixRowBytes(GetPortPixMap(thePort)); /* _cocoa_video_data.realpixels now points to the window's pixels * We want it to point to the *view's* pixels */ { int vOffset = [ _cocoa_video_data.window frame ].size.height - newViewFrame.size.height - newViewFrame.origin.y; int hOffset = newViewFrame.origin.x; _cocoa_video_data.realpixels = (uint8*)_cocoa_video_data.realpixels + (vOffset * _cocoa_video_data.pitch) + hOffset * (_cocoa_video_data.device_bpp / 8); } UnlockPortBits(thePort); /* Allocate new buffer */ free(_cocoa_video_data.pixels); _cocoa_video_data.pixels = (uint8*)malloc(newViewFrame.size.width * newViewFrame.size.height); assert(_cocoa_video_data.pixels != NULL); /* Tell the game that the resolution changed */ _cocoa_video_data.width = newViewFrame.size.width; _cocoa_video_data.height = newViewFrame.size.height; _screen.width = _cocoa_video_data.width; _screen.height = _cocoa_video_data.height; _screen.pitch = _cocoa_video_data.width; GameSizeChanged(); /* Redraw screen */ _cocoa_video_data.num_dirty_rects = MAX_DIRTY_RECTS; } - (void)appDidHide:(NSNotification*)note { _cocoa_video_data.active = false; // DEBUG(driver, 1)("cocoa_v: appDidHide"); } - (void)appWillUnhide:(NSNotification*)note { QZ_SetPortAlphaOpaque (); /* save current visible surface */ [ self cacheImageInRect:[ _cocoa_video_data.qdview frame ] ]; } - (void)appDidUnhide:(NSNotification*)note { /* restore cached image, since it may not be current, post expose event too */ [ self restoreCachedImage ]; _cocoa_video_data.active = true; // DEBUG(driver, 1)("cocoa_v: appDidUnhide"); } - (id)initWithContentRect:(NSRect)contentRect styleMask:(unsigned int)styleMask backing:(NSBackingStoreType)backingType defer:(BOOL)flag { /* Make our window subclass receive these application notifications */ [ [ NSNotificationCenter defaultCenter ] addObserver:self selector:@selector(appDidHide:) name:NSApplicationDidHideNotification object:NSApp ]; [ [ NSNotificationCenter defaultCenter ] addObserver:self selector:@selector(appDidUnhide:) name:NSApplicationDidUnhideNotification object:NSApp ]; [ [ NSNotificationCenter defaultCenter ] addObserver:self selector:@selector(appWillUnhide:) name:NSApplicationWillUnhideNotification object:NSApp ]; return [ super initWithContentRect:contentRect styleMask:styleMask backing:backingType defer:flag ]; } @end @implementation OTTD_QuartzWindowDelegate - (BOOL)windowShouldClose:(id)sender { QZ_AskQuit(); return NO; } - (void)windowDidBecomeKey:(NSNotification*)aNotification { _cocoa_video_data.active = true; // DEBUG(driver, 1)("cocoa_v: windowDidBecomeKey"); } - (void)windowDidResignKey:(NSNotification*)aNotification { _cocoa_video_data.active = false; // DEBUG(driver, 1)("cocoa_v: windowDidResignKey"); } - (void)windowDidBecomeMain:(NSNotification*)aNotification { _cocoa_video_data.active = true; // DEBUG(driver, 1)("cocoa_v: windowDidBecomeMain"); } - (void)windowDidResignMain:(NSNotification*)aNotification { _cocoa_video_data.active = false; // DEBUG(driver, 1)("cocoa_v: windowDidResignMain"); } @end static void QZ_UpdateWindowPalette(uint start, uint count) { uint i; switch (_cocoa_video_data.device_bpp) { case 32: for (i = start; i < start + count; i++) { uint32 clr32 = 0xff000000; clr32 |= (uint32)_cur_palette[i].r << 16; clr32 |= (uint32)_cur_palette[i].g << 8; clr32 |= (uint32)_cur_palette[i].b; _cocoa_video_data.palette32[i] = clr32; } break; case 16: for (i = start; i < start + count; i++) { uint16 clr16 = 0x0000; clr16 |= (uint16)((_cur_palette[i].r >> 3) & 0x1f) << 10; clr16 |= (uint16)((_cur_palette[i].g >> 3) & 0x1f) << 5; clr16 |= (uint16)((_cur_palette[i].b >> 3) & 0x1f); _cocoa_video_data.palette16[i] = clr16; } break; } _cocoa_video_data.num_dirty_rects = MAX_DIRTY_RECTS; } static inline void QZ_WindowBlitIndexedPixelsToView32(int left, int top, int right, int bottom) { int x, y; for (y = top; y < bottom; y++) { const uint8* src = _cocoa_video_data.pixels + y * _cocoa_video_data.width + left; uint32* trg = (uint32*)_cocoa_video_data.realpixels + y * _cocoa_video_data.pitch / 4 + left; for (x = left; x < right; x++, trg++, src++) { *trg = _cocoa_video_data.palette32[*src]; } } } static inline void QZ_WindowBlitIndexedPixelsToView16(int left, int top, int right, int bottom) { int x, y; for (y = top; y < bottom; y++) { const uint8* src = _cocoa_video_data.pixels + y * _cocoa_video_data.width + left; uint16* trg = (uint16*)_cocoa_video_data.realpixels + y * _cocoa_video_data.pitch / 2 + left; for (x = left; x < right; x++, trg++, src++) { *trg = _cocoa_video_data.palette16[*src]; } } } static inline void QZ_WindowBlitIndexedPixelsToView(int left, int top, int right, int bottom) { switch (_cocoa_video_data.device_bpp) { case 32: QZ_WindowBlitIndexedPixelsToView32(left, top, right, bottom); break; case 16: QZ_WindowBlitIndexedPixelsToView16(left, top, right, bottom); break; } } static bool _resize_icon[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0 }; static void QZ_DrawResizeIcon(void) { int xoff = _cocoa_video_data.width - 16; int yoff = _cocoa_video_data.height - 16; int x; int y; for (y = 0; y < 16; y++) { uint16* trg16 = (uint16*)_cocoa_video_data.realpixels + (yoff + y) * _cocoa_video_data.pitch / 2 + xoff; uint32* trg32 = (uint32*)_cocoa_video_data.realpixels + (yoff + y) * _cocoa_video_data.pitch / 4 + xoff; for (x = 0; x < 16; x++, trg16++, trg32++) { if (!_resize_icon[y * 16 + x]) continue; switch (_cocoa_video_data.device_bpp) { case 32: *trg32 = 0xff000000; break; case 16: *trg16 = 0x0000; break; } } } } static void QZ_DrawWindow(void) { int i; RgnHandle dirty, temp; /* Check if we need to do anything */ if (_cocoa_video_data.num_dirty_rects == 0 || [ _cocoa_video_data.window isMiniaturized ]) { return; } if (_cocoa_video_data.num_dirty_rects >= MAX_DIRTY_RECTS) { _cocoa_video_data.num_dirty_rects = 1; _cocoa_video_data.dirty_rects[0].left = 0; _cocoa_video_data.dirty_rects[0].top = 0; _cocoa_video_data.dirty_rects[0].right = _cocoa_video_data.width; _cocoa_video_data.dirty_rects[0].bottom = _cocoa_video_data.height; } dirty = NewRgn(); temp = NewRgn(); SetEmptyRgn(dirty); /* Build the region of dirty rectangles */ for (i = 0; i < _cocoa_video_data.num_dirty_rects; i++) { QZ_WindowBlitIndexedPixelsToView( _cocoa_video_data.dirty_rects[i].left, _cocoa_video_data.dirty_rects[i].top, _cocoa_video_data.dirty_rects[i].right, _cocoa_video_data.dirty_rects[i].bottom ); MacSetRectRgn( temp, _cocoa_video_data.dirty_rects[i].left, _cocoa_video_data.dirty_rects[i].top, _cocoa_video_data.dirty_rects[i].right, _cocoa_video_data.dirty_rects[i].bottom ); MacUnionRgn(dirty, temp, dirty); } QZ_DrawResizeIcon(); /* Flush the dirty region */ QDFlushPortBuffer([ _cocoa_video_data.qdview qdPort ], dirty); DisposeRgn(dirty); DisposeRgn(temp); _cocoa_video_data.num_dirty_rects = 0; } extern const char _openttd_revision[]; static const char* QZ_SetVideoWindowed(uint width, uint height) { char caption[50]; NSString *nsscaption; unsigned int style; NSRect contentRect; BOOL isCustom = NO; if (width > _cocoa_video_data.device_width) width = _cocoa_video_data.device_width; if (height > _cocoa_video_data.device_height) height = _cocoa_video_data.device_height; _cocoa_video_data.width = width; _cocoa_video_data.height = height; contentRect = NSMakeRect(0, 0, width, height); /* Check if we should completely destroy the previous mode * - If it is fullscreen */ if (_cocoa_video_data.isset && _cocoa_video_data.fullscreen) QZ_UnsetVideoMode(); /* Check if we should recreate the window */ if (_cocoa_video_data.window == nil) { /* Set the window style */ style = NSTitledWindowMask; style |= (NSMiniaturizableWindowMask | NSClosableWindowMask); style |= NSResizableWindowMask; /* Manually create a window, avoids having a nib file resource */ _cocoa_video_data.window = [ [ OTTD_QuartzWindow alloc ] initWithContentRect:contentRect styleMask:style backing:NSBackingStoreBuffered defer:NO ]; if (_cocoa_video_data.window == nil) return "Could not create the Cocoa window"; snprintf(caption, sizeof(caption), "OpenTTD %s", _openttd_revision); nsscaption = [ [ NSString alloc ] initWithCString:caption ]; [ _cocoa_video_data.window setTitle:nsscaption ]; [ _cocoa_video_data.window setMiniwindowTitle:nsscaption ]; [ nsscaption release ]; [ _cocoa_video_data.window setAcceptsMouseMovedEvents:YES ]; [ _cocoa_video_data.window setViewsNeedDisplay:NO ]; [ _cocoa_video_data.window setDelegate: [ [ [ OTTD_QuartzWindowDelegate alloc ] init ] autorelease ] ]; } else { /* We already have a window, just change its size */ if (!isCustom) { [ _cocoa_video_data.window setContentSize:contentRect.size ]; [ _cocoa_video_data.qdview setFrameSize:contentRect.size ]; } } [ _cocoa_video_data.window center ]; /* Only recreate the view if it doesn't already exist */ if (_cocoa_video_data.qdview == nil) { _cocoa_video_data.qdview = [ [ NSQuickDrawView alloc ] initWithFrame:contentRect ]; [ _cocoa_video_data.qdview setAutoresizingMask: NSViewWidthSizable | NSViewHeightSizable ]; [ [ _cocoa_video_data.window contentView ] addSubview:_cocoa_video_data.qdview ]; [ _cocoa_video_data.qdview release ]; [ _cocoa_video_data.window makeKeyAndOrderFront:nil ]; } LockPortBits([ _cocoa_video_data.qdview qdPort ]); _cocoa_video_data.realpixels = GetPixBaseAddr(GetPortPixMap([ _cocoa_video_data.qdview qdPort ])); _cocoa_video_data.pitch = GetPixRowBytes(GetPortPixMap([ _cocoa_video_data.qdview qdPort ])); UnlockPortBits([ _cocoa_video_data.qdview qdPort ]); /* _cocoa_video_data.realpixels now points to the window's pixels * We want it to point to the *view's* pixels */ { int vOffset = [ _cocoa_video_data.window frame ].size.height - [ _cocoa_video_data.qdview frame ].size.height - [ _cocoa_video_data.qdview frame ].origin.y; int hOffset = [ _cocoa_video_data.qdview frame ].origin.x; _cocoa_video_data.realpixels = (uint8*)_cocoa_video_data.realpixels + (vOffset * _cocoa_video_data.pitch) + hOffset * (_cocoa_video_data.device_bpp / 8); } free(_cocoa_video_data.pixels); _cocoa_video_data.pixels = (uint8*)malloc(width * height); if (_cocoa_video_data.pixels == NULL) return "Failed to allocate 8-bit buffer"; _cocoa_video_data.fullscreen = false; return NULL; } /****************************************************************************** * Fullscreen mode * ******************************************************************************/ /* Gamma functions to try to hide the flash from a rez switch * Fade the display from normal to black * Save gamma tables for fade back to normal */ static uint32 QZ_FadeGammaOut(const OTTD_QuartzGammaTable* table) { CGGammaValue redTable[QZ_GAMMA_TABLE_SIZE]; CGGammaValue greenTable[QZ_GAMMA_TABLE_SIZE]; CGGammaValue blueTable[QZ_GAMMA_TABLE_SIZE]; float percent; int j; unsigned int actual; if (CGGetDisplayTransferByTable( _cocoa_video_data.display_id, QZ_GAMMA_TABLE_SIZE, table->red, table->green, table->blue, &actual ) != CGDisplayNoErr || actual != QZ_GAMMA_TABLE_SIZE) { return 1; } memcpy(redTable, table->red, sizeof(redTable)); memcpy(greenTable, table->green, sizeof(greenTable)); memcpy(blueTable, table->blue, sizeof(greenTable)); for (percent = 1.0; percent >= 0.0; percent -= 0.01) { for (j = 0; j < QZ_GAMMA_TABLE_SIZE; j++) { redTable[j] = redTable[j] * percent; greenTable[j] = greenTable[j] * percent; blueTable[j] = blueTable[j] * percent; } if (CGSetDisplayTransferByTable( _cocoa_video_data.display_id, QZ_GAMMA_TABLE_SIZE, redTable, greenTable, blueTable ) != CGDisplayNoErr) { CGDisplayRestoreColorSyncSettings(); return 1; } CSleep(10); } return 0; } /* Fade the display from black to normal * Restore previously saved gamma values */ static uint32 QZ_FadeGammaIn(const OTTD_QuartzGammaTable* table) { CGGammaValue redTable[QZ_GAMMA_TABLE_SIZE]; CGGammaValue greenTable[QZ_GAMMA_TABLE_SIZE]; CGGammaValue blueTable[QZ_GAMMA_TABLE_SIZE]; float percent; int j; memset(redTable, 0, sizeof(redTable)); memset(greenTable, 0, sizeof(greenTable)); memset(blueTable, 0, sizeof(greenTable)); for (percent = 0.0; percent <= 1.0; percent += 0.01) { for (j = 0; j < QZ_GAMMA_TABLE_SIZE; j++) { redTable[j] = table->red[j] * percent; greenTable[j] = table->green[j] * percent; blueTable[j] = table->blue[j] * percent; } if (CGSetDisplayTransferByTable( _cocoa_video_data.display_id, QZ_GAMMA_TABLE_SIZE, redTable, greenTable, blueTable ) != CGDisplayNoErr) { CGDisplayRestoreColorSyncSettings(); return 1; } CSleep(10); } return 0; } static const char* QZ_SetVideoFullScreen(int width, int height) { const char* errstr = "QZ_SetVideoFullScreen error"; int exact_match; CFNumberRef number; int bpp; int gamma_error; OTTD_QuartzGammaTable gamma_table; NSRect screen_rect; CGError error; NSPoint pt; /* Destroy any previous mode */ if (_cocoa_video_data.isset) QZ_UnsetVideoMode(); /* See if requested mode exists */ _cocoa_video_data.mode = CGDisplayBestModeForParameters(_cocoa_video_data.display_id, 8, width, height, &exact_match); /* If the mode wasn't an exact match, check if it has the right bpp, and update width and height */ if (!exact_match) { number = CFDictionaryGetValue (_cocoa_video_data.mode, kCGDisplayBitsPerPixel); CFNumberGetValue(number, kCFNumberSInt32Type, &bpp); if (bpp != 8) { errstr = "Failed to find display resolution"; goto ERR_NO_MATCH; } number = CFDictionaryGetValue(_cocoa_video_data.mode, kCGDisplayWidth); CFNumberGetValue(number, kCFNumberSInt32Type, &width); number = CFDictionaryGetValue(_cocoa_video_data.mode, kCGDisplayHeight); CFNumberGetValue(number, kCFNumberSInt32Type, &height); } /* Fade display to zero gamma */ gamma_error = QZ_FadeGammaOut(&gamma_table); /* Put up the blanking window (a window above all other windows) */ error = CGDisplayCapture(_cocoa_video_data.display_id); if (CGDisplayNoErr != error) { errstr = "Failed capturing display"; goto ERR_NO_CAPTURE; } /* Do the physical switch */ if (CGDisplaySwitchToMode(_cocoa_video_data.display_id, _cocoa_video_data.mode) != CGDisplayNoErr) { errstr = "Failed switching display resolution"; goto ERR_NO_SWITCH; } _cocoa_video_data.realpixels = (uint8*)CGDisplayBaseAddress(_cocoa_video_data.display_id); _cocoa_video_data.pitch = CGDisplayBytesPerRow(_cocoa_video_data.display_id); _cocoa_video_data.width = CGDisplayPixelsWide(_cocoa_video_data.display_id); _cocoa_video_data.height = CGDisplayPixelsHigh(_cocoa_video_data.display_id); _cocoa_video_data.fullscreen = true; /* Setup double-buffer emulation */ _cocoa_video_data.pixels = (uint8*)malloc(width * height); if (_cocoa_video_data.pixels == NULL) { errstr = "Failed to allocate memory for double buffering"; goto ERR_DOUBLEBUF; } if (!CGDisplayCanSetPalette(_cocoa_video_data.display_id)) { errstr = "Not an indexed display mode."; goto ERR_NOT_INDEXED; } /* If we don't hide menu bar, it will get events and interrupt the program */ HideMenuBar(); /* Fade the display to original gamma */ if (!gamma_error) QZ_FadeGammaIn(&gamma_table); /* There is a bug in Cocoa where NSScreen doesn't synchronize * with CGDirectDisplay, so the main screen's frame is wrong. * As a result, coordinate translation produces incorrect results. * We can hack around this bug by setting the screen rect ourselves. * This hack should be removed if/when the bug is fixed. */ screen_rect = NSMakeRect(0, 0, width, height); [ [ NSScreen mainScreen ] setFrame:screen_rect ]; /* we're fullscreen, so flag all input states... */ _cocoa_video_data.active = true; pt = [ NSEvent mouseLocation ]; pt.y = CGDisplayPixelsHigh(_cocoa_video_data.display_id) - pt.y; if (QZ_MouseIsInsideView(&pt)) QZ_HideMouse(); return NULL; /* Since the blanking window covers *all* windows (even force quit) correct recovery is crucial */ ERR_NOT_INDEXED: free(_cocoa_video_data.pixels); _cocoa_video_data.pixels = NULL; ERR_DOUBLEBUF: CGDisplaySwitchToMode(_cocoa_video_data.display_id, _cocoa_video_data.save_mode); ERR_NO_SWITCH: CGReleaseAllDisplays(); ERR_NO_CAPTURE: if (!gamma_error) QZ_FadeGammaIn(&gamma_table); ERR_NO_MATCH: return errstr; } static void QZ_UpdateFullscreenPalette(uint first_color, uint num_colors) { CGTableCount index; CGDeviceColor color; for (index = first_color; index < first_color+num_colors; index++) { /* Clamp colors between 0.0 and 1.0 */ color.red = _cur_palette[index].r / 255.0; color.blue = _cur_palette[index].b / 255.0; color.green = _cur_palette[index].g / 255.0; CGPaletteSetColorAtIndex(_cocoa_video_data.palette, color, index); } CGDisplaySetPalette(_cocoa_video_data.display_id, _cocoa_video_data.palette); } /* Wait for the VBL to occur (estimated since we don't have a hardware interrupt) */ static void QZ_WaitForVerticalBlank(void) { /* The VBL delay is based on Ian Ollmann's RezLib */ double refreshRate; double linesPerSecond; double target; double position; double adjustment; CFNumberRef refreshRateCFNumber; refreshRateCFNumber = CFDictionaryGetValue(_cocoa_video_data.mode, kCGDisplayRefreshRate); if (refreshRateCFNumber == NULL) return; if (CFNumberGetValue(refreshRateCFNumber, kCFNumberDoubleType, &refreshRate) == 0) return; if (refreshRate == 0) return; linesPerSecond = refreshRate * _cocoa_video_data.height; target = _cocoa_video_data.height; /* Figure out the first delay so we start off about right */ position = CGDisplayBeamPosition(_cocoa_video_data.display_id); if (position > target) position = 0; adjustment = (target - position) / linesPerSecond; CSleep((uint32)(adjustment * 1000)); } static void QZ_DrawScreen(void) { uint y; QZ_WaitForVerticalBlank(); for (y = 0; y < _cocoa_video_data.height; y++) { const uint8* src = _cocoa_video_data.pixels + y * _cocoa_video_data.width; uint8* dst = (uint8*)_cocoa_video_data.realpixels + y * _cocoa_video_data.pitch; memcpy(dst, src, _cocoa_video_data.width); } } static int QZ_ListFullscreenModes(OTTDPoint* mode_list, int max_modes) { CFIndex num_modes; CFIndex i; int list_size = 0; num_modes = CFArrayGetCount(_cocoa_video_data.mode_list); /* Build list of modes with the requested bpp */ for (i = 0; i < num_modes && list_size < max_modes; i++) { CFDictionaryRef onemode; CFNumberRef number; int bpp; int intvalue; bool hasMode; uint16 width, height; onemode = CFArrayGetValueAtIndex(_cocoa_video_data.mode_list, i); number = CFDictionaryGetValue(onemode, kCGDisplayBitsPerPixel); CFNumberGetValue (number, kCFNumberSInt32Type, &bpp); if (bpp != 8) continue; number = CFDictionaryGetValue(onemode, kCGDisplayWidth); CFNumberGetValue(number, kCFNumberSInt32Type, &intvalue); width = (uint16)intvalue; number = CFDictionaryGetValue(onemode, kCGDisplayHeight); CFNumberGetValue(number, kCFNumberSInt32Type, &intvalue); height = (uint16)intvalue; /* Check if mode is already in the list */ { int i; hasMode = false; for (i = 0; i < list_size; i++) { if (mode_list[i].x == width && mode_list[i].y == height) { hasMode = true; break; } } } if (hasMode) continue; /* Add mode to the list */ mode_list[list_size].x = width; mode_list[list_size].y = height; list_size++; } /* Sort list smallest to largest */ { int i, j; for (i = 0; i < list_size; i++) { for (j = 0; j < list_size-1; j++) { if (mode_list[j].x > mode_list[j + 1].x || ( mode_list[j].x == mode_list[j + 1].x && mode_list[j].y > mode_list[j + 1].y )) { uint tmpw = mode_list[j].x; uint tmph = mode_list[j].y; mode_list[j].x = mode_list[j + 1].x; mode_list[j].y = mode_list[j + 1].y; mode_list[j + 1].x = tmpw; mode_list[j + 1].y = tmph; } } } } return list_size; } /****************************************************************************** * Windowed and fullscreen common code * ******************************************************************************/ static void QZ_UpdatePalette(uint start, uint count) { if (_cocoa_video_data.fullscreen) { QZ_UpdateFullscreenPalette(start, count); } else { QZ_UpdateWindowPalette(start, count); } } static void QZ_InitPalette(void) { QZ_UpdatePalette(0, 256); } static void QZ_Draw(void) { if (_cocoa_video_data.fullscreen) { QZ_DrawScreen(); } else { QZ_DrawWindow(); } } static const OTTDPoint _default_resolutions[] = { { 640, 480}, { 800, 600}, {1024, 768}, {1152, 864}, {1280, 800}, {1280, 960}, {1280, 1024}, {1400, 1050}, {1600, 1200}, {1680, 1050}, {1920, 1200} }; static void QZ_UpdateVideoModes(void) { uint i, j, count; OTTDPoint modes[32]; const OTTDPoint *current_modes; if (_cocoa_video_data.fullscreen) { count = QZ_ListFullscreenModes(modes, 32); current_modes = modes; } else { count = lengthof(_default_resolutions); current_modes = _default_resolutions; } for (i = 0, j = 0; j < lengthof(_resolutions) && i < count; i++) { if (_cocoa_video_data.fullscreen || ( (uint)current_modes[i].x <= _cocoa_video_data.device_width && (uint)current_modes[i].y <= _cocoa_video_data.device_height) ) { _resolutions[j][0] = current_modes[i].x; _resolutions[j][1] = current_modes[i].y; j++; } } _num_resolutions = j; } static void QZ_UnsetVideoMode(void) { if (_cocoa_video_data.fullscreen) { /* Release fullscreen resources */ OTTD_QuartzGammaTable gamma_table; int gamma_error; NSRect screen_rect; gamma_error = QZ_FadeGammaOut(&gamma_table); /* Restore original screen resolution/bpp */ CGDisplaySwitchToMode(_cocoa_video_data.display_id, _cocoa_video_data.save_mode); CGReleaseAllDisplays(); ShowMenuBar(); /* Reset the main screen's rectangle * See comment in QZ_SetVideoFullscreen for why we do this */ screen_rect = NSMakeRect(0,0,_cocoa_video_data.device_width,_cocoa_video_data.device_height); [ [ NSScreen mainScreen ] setFrame:screen_rect ]; if (!gamma_error) QZ_FadeGammaIn(&gamma_table); } else { /* Release window mode resources */ [ _cocoa_video_data.window close ]; _cocoa_video_data.window = nil; _cocoa_video_data.qdview = nil; } free(_cocoa_video_data.pixels); _cocoa_video_data.pixels = NULL; QZ_ShowMouse(); /* Signal successful teardown */ _cocoa_video_data.isset = false; } static const char* QZ_SetVideoMode(uint width, uint height, bool fullscreen) { const char *ret; _cocoa_video_data.issetting = true; if (fullscreen) { /* Setup full screen video */ ret = QZ_SetVideoFullScreen(width, height); } else { /* Setup windowed video */ ret = QZ_SetVideoWindowed(width, height); } _cocoa_video_data.issetting = false; if (ret != NULL) return ret; /* Signal successful completion (used internally) */ _cocoa_video_data.isset = true; /* Tell the game that the resolution has changed */ _screen.width = _cocoa_video_data.width; _screen.height = _cocoa_video_data.height; _screen.pitch = _cocoa_video_data.width; QZ_UpdateVideoModes(); GameSizeChanged(); QZ_InitPalette(); return NULL; } static const char* QZ_SetVideoModeAndRestoreOnFailure(uint width, uint height, bool fullscreen) { bool wasset = _cocoa_video_data.isset; uint32 oldwidth = _cocoa_video_data.width; uint32 oldheight = _cocoa_video_data.height; bool oldfullscreen = _cocoa_video_data.fullscreen; const char *ret; ret = QZ_SetVideoMode(width, height, fullscreen); if (ret != NULL && wasset) QZ_SetVideoMode(oldwidth, oldheight, oldfullscreen); return ret; } static void QZ_VideoInit(void) { memset(&_cocoa_video_data, 0, sizeof(_cocoa_video_data)); /* Initialize the video settings; this data persists between mode switches */ _cocoa_video_data.display_id = kCGDirectMainDisplay; _cocoa_video_data.save_mode = CGDisplayCurrentMode(_cocoa_video_data.display_id); _cocoa_video_data.mode_list = CGDisplayAvailableModes(_cocoa_video_data.display_id); _cocoa_video_data.palette = CGPaletteCreateDefaultColorPalette(); /* Gather some information that is useful to know about the display */ /* Maybe this should be moved to QZ_SetVideoMode, in case this is changed after startup */ CFNumberGetValue( CFDictionaryGetValue(_cocoa_video_data.save_mode, kCGDisplayBitsPerPixel), kCFNumberSInt32Type, &_cocoa_video_data.device_bpp ); CFNumberGetValue( CFDictionaryGetValue(_cocoa_video_data.save_mode, kCGDisplayWidth), kCFNumberSInt32Type, &_cocoa_video_data.device_width ); CFNumberGetValue( CFDictionaryGetValue(_cocoa_video_data.save_mode, kCGDisplayHeight), kCFNumberSInt32Type, &_cocoa_video_data.device_height ); _cocoa_video_data.cursor_visible = true; /* register for sleep notifications so wake from sleep generates SDL_VIDEOEXPOSE */ // QZ_RegisterForSleepNotifications(); } /* Convert local coordinate to window server (CoreGraphics) coordinate */ static CGPoint QZ_PrivateLocalToCG(NSPoint* p) { CGPoint cgp; if (!_cocoa_video_data.fullscreen) { *p = [ _cocoa_video_data.qdview convertPoint:*p toView: nil ]; *p = [ _cocoa_video_data.window convertBaseToScreen:*p ]; p->y = _cocoa_video_data.device_height - p->y; } cgp.x = p->x; cgp.y = p->y; return cgp; } static void QZ_WarpCursor(int x, int y) { NSPoint p; CGPoint cgp; /* Only allow warping when in foreground */ if (![ NSApp isActive ]) return; p = NSMakePoint(x, y); cgp = QZ_PrivateLocalToCG(&p); /* this is the magic call that fixes cursor "freezing" after warp */ CGSetLocalEventsSuppressionInterval(0.0); /* Do the actual warp */ CGWarpMouseCursorPosition(cgp); /* Generate the mouse moved event */ // SDL_PrivateMouseMotion(0, 0, x, y); } static void QZ_ShowMouse(void) { if (!_cocoa_video_data.cursor_visible) { [ NSCursor unhide ]; _cocoa_video_data.cursor_visible = true; } } static void QZ_HideMouse(void) { if (_cocoa_video_data.cursor_visible) { #ifndef _DEBUG [ NSCursor hide ]; #endif _cocoa_video_data.cursor_visible = false; } } /****************************************************************************** * OS X application creation * ******************************************************************************/ /* The main class of the application, the application's delegate */ @implementation OTTDMain /* Called when the internal event loop has just started running */ - (void) applicationDidFinishLaunching: (NSNotification*) note { /* Hand off to main application code */ QZ_GameLoop(); /* We're done, thank you for playing */ [ NSApp stop:_ottd_main ]; } /* Display the in game quit confirmation dialog */ - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*) sender { // DEBUG(driver, 1)("cocoa_v: applicationShouldTerminate"); QZ_AskQuit(); return NSTerminateCancel; // NSTerminateLater ? } @end static void setApplicationMenu(void) { /* warning: this code is very odd */ NSMenu *appleMenu; NSMenuItem *menuItem; NSString *title; NSString *appName; appName = @"OTTD"; appleMenu = [[NSMenu alloc] initWithTitle:appName]; /* Add menu items */ title = [@"About " stringByAppendingString:appName]; [appleMenu addItemWithTitle:title action:@selector(orderFrontStandardAboutPanel:) keyEquivalent:@""]; [appleMenu addItem:[NSMenuItem separatorItem]]; title = [@"Hide " stringByAppendingString:appName]; [appleMenu addItemWithTitle:title action:@selector(hide:) keyEquivalent:@"h"]; menuItem = (NSMenuItem*)[appleMenu addItemWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"]; [menuItem setKeyEquivalentModifierMask:(NSAlternateKeyMask|NSCommandKeyMask)]; [appleMenu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""]; [appleMenu addItem:[NSMenuItem separatorItem]]; title = [@"Quit " stringByAppendingString:appName]; [appleMenu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q"]; /* Put menu into the menubar */ menuItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]; [menuItem setSubmenu:appleMenu]; [[NSApp mainMenu] addItem:menuItem]; /* Tell the application object that this is now the application menu */ [NSApp setAppleMenu:appleMenu]; /* Finally give up our references to the objects */ [appleMenu release]; [menuItem release]; } /* Create a window menu */ static void setupWindowMenu(void) { NSMenu* windowMenu; NSMenuItem* windowMenuItem; NSMenuItem* menuItem; windowMenu = [[NSMenu alloc] initWithTitle:@"Window"]; /* "Minimize" item */ menuItem = [[NSMenuItem alloc] initWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"]; [windowMenu addItem:menuItem]; [menuItem release]; /* Put menu into the menubar */ windowMenuItem = [[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""]; [windowMenuItem setSubmenu:windowMenu]; [[NSApp mainMenu] addItem:windowMenuItem]; /* Tell the application object that this is now the window menu */ [NSApp setWindowsMenu:windowMenu]; /* Finally give up our references to the objects */ [windowMenu release]; [windowMenuItem release]; } static void setupApplication(void) { CPSProcessSerNum PSN; /* Ensure the application object is initialised */ [NSApplication sharedApplication]; /* Tell the dock about us */ if (!CPSGetCurrentProcess(&PSN) && !CPSEnableForegroundOperation(&PSN, 0x03, 0x3C, 0x2C, 0x1103) && !CPSSetFrontProcess(&PSN)) { [NSApplication sharedApplication]; } /* Set up the menubar */ [NSApp setMainMenu:[[NSMenu alloc] init]]; setApplicationMenu(); setupWindowMenu(); /* Create OTTDMain and make it the app delegate */ _ottd_main = [[OTTDMain alloc] init]; [NSApp setDelegate:_ottd_main]; } /****************************************************************************** * Video driver interface * ******************************************************************************/ static void CocoaVideoStop(void) { DEBUG(driver, 1)("cocoa_v: CocoaVideoStop"); if (!_cocoa_video_started) return; if (_cocoa_video_data.isset) QZ_UnsetVideoMode(); [_ottd_main release]; _cocoa_video_started = false; } static const char *CocoaVideoStart(const char * const *parm) { const char *ret; DEBUG(driver, 1)("cocoa_v: CocoaVideoStart"); if (_cocoa_video_started) return "Already started"; _cocoa_video_started = true; memset(&_cocoa_video_data, 0, sizeof(_cocoa_video_data)); setupApplication(); /* Don't create a window or enter fullscreen if we're just going to show a dialog. */ if (_cocoa_video_dialog) return NULL; QZ_VideoInit(); ret = QZ_SetVideoMode(_cur_resolution[0], _cur_resolution[1], _fullscreen); if (ret != NULL) CocoaVideoStop(); return ret; } static void CocoaVideoMakeDirty(int left, int top, int width, int height) { if (_cocoa_video_data.num_dirty_rects < MAX_DIRTY_RECTS) { _cocoa_video_data.dirty_rects[_cocoa_video_data.num_dirty_rects].left = left; _cocoa_video_data.dirty_rects[_cocoa_video_data.num_dirty_rects].top = top; _cocoa_video_data.dirty_rects[_cocoa_video_data.num_dirty_rects].right = left + width; _cocoa_video_data.dirty_rects[_cocoa_video_data.num_dirty_rects].bottom = top + height; } _cocoa_video_data.num_dirty_rects++; } static void CocoaVideoMainLoop(void) { DEBUG(driver, 1)("cocoa_v: CocoaVideoMainLoop"); /* Start the main event loop */ [NSApp run]; } static bool CocoaVideoChangeRes(int w, int h) { const char *ret; DEBUG(driver, 1)("cocoa_v: CocoaVideoChangeRes"); ret = QZ_SetVideoModeAndRestoreOnFailure((uint)w, (uint)h, _cocoa_video_data.fullscreen); if (ret != NULL) { DEBUG(driver, 1)("cocoa_v: failed with message: %s", ret); } return ret == NULL; } static void CocoaVideoFullScreen(bool full_screen) { const char *ret; DEBUG(driver, 1)("cocoa_v: CocoaVideoFullScreen"); ret = QZ_SetVideoModeAndRestoreOnFailure(_cocoa_video_data.width, _cocoa_video_data.height, full_screen); if (ret != NULL) { DEBUG(driver, 1)("cocoa_v: failed with message: %s", ret); } _fullscreen = _cocoa_video_data.fullscreen; } const HalVideoDriver _cocoa_video_driver = { CocoaVideoStart, CocoaVideoStop, CocoaVideoMakeDirty, CocoaVideoMainLoop, CocoaVideoChangeRes, CocoaVideoFullScreen, }; /* This is needed since sometimes assert is called before the videodriver is initialized */ void CocoaDialog(const char* title, const char* message, const char* buttonLabel) { bool wasstarted; _cocoa_video_dialog = true; wasstarted = _cocoa_video_started; if (!_cocoa_video_started && CocoaVideoStart(NULL) != NULL) { fprintf(stderr, "%s: %s\n", title, message); return; } NSRunAlertPanel([NSString stringWithCString: title], [NSString stringWithCString: message], [NSString stringWithCString: buttonLabel], nil, nil); if (!wasstarted) CocoaVideoStop(); _cocoa_video_dialog = false; } /* This is needed since OS X applications are started with the working dir set to / when double-clicked */ void cocoaSetWorkingDirectory(void) { char parentdir[MAXPATHLEN]; int chdir_ret; CFURLRef url = CFBundleCopyBundleURL(CFBundleGetMainBundle()); CFURLRef url2 = CFURLCreateCopyDeletingLastPathComponent(0, url); if (CFURLGetFileSystemRepresentation(url2, true, (unsigned char*)parentdir, MAXPATHLEN)) { chdir_ret = chdir(parentdir); /* chdir to the binary app's parent */ assert(chdir_ret == 0); } CFRelease(url); CFRelease(url2); } /* These are called from main() to prevent a _NSAutoreleaseNoPool error when * exiting before the cocoa video driver has been loaded */ void cocoaSetupAutoreleasePool(void) { _ottd_autorelease_pool = [[NSAutoreleasePool alloc] init]; } void cocoaReleaseAutoreleasePool(void) { [_ottd_autorelease_pool release]; } #endif /* WITH_COCOA */